JavaScript中对象的复制、浅拷贝、深拷贝

简介: JavaScript中对象的复制、浅拷贝、深拷贝

数据类型与堆栈的关系


JS数据类型:JS 的数据类型有几种?


8种,NumberStringBooleanNullundefinedobject、symbol、bigInt

Symbol


Symbol 本质上是一种唯一标识符,可用作对象的唯一属性名,这样其他人就不会改写或覆盖你设置的属性值


Symbol 数据类型的特点是唯一性,即使是用同一个变量生成的值也不相等


let id1 = Symbol('id'); 
let id2 = Symbol('id'); 
console.log(id1 == id2); //false


Symbol 数据类型的另一特点是隐藏性,for···in,object.keys() 不能访问


let id = Symbol("id"); 
let obj = { [id]:'symbol' }; 
for(let option in obj){ 
    console.log(obj[option]); //空 
}


但是也有能够访问的方法:Object.getOwnPropertySymbols

Object.getOwnPropertySymbols 方法会返回一个数组,成员是当前对象的所有用作属性名的 Symbol 值


JS数据类型:Object 中包含了哪几种类型?


其中包含了Object,Array,Date,Function,RegExp


JS数据类型:JS的基本类型有哪些呢?


基本类型(单类型):StringNumberbooleannullundefined


BigInt


谷歌67版本中出现了一种bigInt,是指安全存储、操作大整数。(但是很多人不把这个做为一个类型)


BigInt数据类型的目的是比Number数据类型支持的范围更大的整数值。在对大整数执行数学运算时,以任意精度表示整数的能力尤为重要。使用BigInt,整数溢出将不再是问题。具体请看JavaScript 标准内置对象, JS最新基本数据类型:BigInt


存储方式


基本类型:基本类型值在内存中占据固定大小,保存在`栈内存`中(不包含`闭包`中的变量)


引用类型:引用类型的值是对象,保存在`堆内存`中。而栈内存存储的是对象的变量标识符以及对象在堆内存中的存储地址(引用),引用数据类型在栈中存储了指针,该指针指向堆中该实体的起始地址。当解释器寻找引用值时,会首先检索其在栈中的地址,取得地址后从堆中获得实体。


这里用一张图表明它们之前储存的关系


image.png

闭包与堆内存


闭包中的变量并不保存中栈内存中,而是保存在堆内存中。 这也就解释了函数调用之后之后为什么闭包还能引用到函数内的变量。


我们先来看什么是闭包:


function A() {
  let a = '羊先生'
  function B() {
      console.log(a)
  }
  return B
}


函数 A 返回了一个函数 B,并且函数 B 中使用了函数 A 的变量,函数 B 就被称为闭包。

函数 A 调用后,函数 A 中的变量这时候是存储在堆上的,所以函数B依旧能引用到函数A中的变量


赋值


基本数据类型复制


let a ='羊先生';
let b = a;
b='www.vipbic.com';
console.log(a); // 羊先生


结论:在栈内存中的数据发生数据变化的时候,系统会自动为新的变量分配一个新的之值在栈内存中,两个变量相互独立,互不影响的。


引用数据类型复制


let a = {x:'羊先生', y:'羊先生1'}
let b = a;
b.x = 'www.vipbic.com';
console.log(a.x); // www.vipbic.com


结论 引用类型的复制,同样为新的变量b分配一个新的值,保存在栈内存中,不同的是这个变量对应的具体值不在栈中,栈中只是一个地址指针。两个变量地址指针相同,指向堆内存中的对象,因此b.x发生改变的时候,a.x也发生了改变


浅拷贝


Array的slice和concat方法


Array的slice和concat方法不修改原数组,只会返回一个浅复制了原数组中的元素的一个新数组。之所以把它放在浅拷贝里,是因为它看起来像是深拷贝。而实际上它是浅拷贝。原数组的元素会按照下述规则拷贝:


var a = [ 1, 3, 5, { x: 1 } ];
var b = Array.prototype.slice.call(a);
b[0] = 2;
console.log(a); // [ 1, 3, 5, { x: 1 } ];
console.log(b); // [ 2, 3, 5, { x: 1 } ];


从输出结果可以看出,浅拷贝后,数组a[0]并不会随着b[0]改变而改变,说明a和b在栈内存中引用地址并不相同。


var a = [ 1, 3, 5, { x: 1 } ];
var b = Array.prototype.slice.call(a);
b[3].x = 2;
console.log(a); // [ 1, 3, 5, { x: 2 } ];
console.log(b); // [ 1, 3, 5, { x: 2 } ];


从输出结果可以看出,浅拷贝后,数组中对象的属性会根据修改而改变,说明浅拷贝的时候拷贝的已存在对象的对象的属性引用


...扩展运算符


语法: var cloneObj = { ...obj };
let obj = {a:1,b:{c:1}}
let obj2 = {...obj};
obj.a=2;
console.log(obj); //{a:2,b:{c:1}}
console.log(obj2); //{a:1,b:{c:1}}
obj.b.c = 2;
console.log(obj); //{a:2,b:{c:2}}
console.log(obj2); //{a:1,b:{c:2}}


扩展运算符也是浅拷贝,对于值是对象的属性无法完全拷贝成2个不同对象


实现简单的引用复制


function shallowClone(copyObj) {
  var obj = {};
  for ( var i in copyObj) {
    obj[i] = copyObj[i];
  }
  return obj;
}
var x = {
  a: 1,
  b: { f: { g: 1 } },
  c: [ 1, 2, 3 ]
};
var y = shallowClone(x);
console.log(y.b.f === x.b.f);     // true


Object.assign()


object.assign() 方法可以把任意多个的源对象自身的可枚举属性拷贝给目标对象,然后返回目标对象。


var obj1 = {
    title: '测试',
    subset: {
        name: '子对象',
        subpoll: {
            des: '子子对象'
        },
        sex: function() {
            console.log('sex')
        }
    },
    age: function() {
        console.log('age')
    }
}
//只从表面上来看,似乎Object.assign()的目标对象是{},是一个新的对象(开辟了一块新的内存空间),是深拷贝
var ojb2 = Object.assign({}, obj1)
ojb2.title = '1111' // obj1 不受影响 
ojb2.subset.name = '2222' // obj1 受影响 
ojb2.subset.subpoll.des = '3333' // obj1 受影响 
// 打印看结果
console.log('obj1:', obj1)
console.log('ojb2:', ojb2)


image.png


从打印结果可以看出,Object.assign是一个浅拷贝,它只是在根属性(对象的第一层级)创建了一个新的对象,但是对于属性的值是对象的话只会拷贝一份相同的内存地址。


Object.assign注意事项


1.只拷贝源对象的自身属性(不拷贝继承属性)

2.它不会拷贝对象不可枚举的属性

3.undefinednull无法转成对象,它们不能作为Object.assign参数,但是可以作为源对象


Object.assign(undefined) // 报错
Object.assign(null) // 报错
let obj = {a: 1};
Object.assign(obj, undefined) === obj // true
Object.assign(obj, null) === obj // true


4.属性名为Symbol值的属性,可以被Object.assign拷贝


深拷贝


JSON.parse(JSON.stringify())


JSON.stringify()是前端开发过程中比较常用的深拷贝方式。原理是把一个对象序列化成为一个JSON字符串,将对象的内容转换成字符串的形式再保存在磁盘上,再用JSON.parse()反序列化将JSON字符串变成一个新的对象


let arr = [1, 3, {
    username: '羊先生'
}];
let arr4 = JSON.parse(JSON.stringify(arr));
arr4[2].username = 'www.vipbic.com'; 
console.log(arr4);// [ 1, 3, { username: 'www.vipbic.com' } ]
console.log(arr);// [ 1, 3, { username: ' 羊先生' } ]


实现了深拷贝,当改变数组中对象的值时候,原数组中的内容并没有发生改变。


JSON.stringify()虽然可以实现深拷贝,但是还有一些弊端比如不能处理函数等。


JSON.stringify()实现深拷贝注意点


1.拷贝的对象的值中如果有函数,undefined,symbol则经过JSON.stringify()序列化后的JSON字符串中这个键值对会消失

2.无法拷贝不可枚举的属性,无法拷贝对象的原型链

3.拷贝Date引用类型会变成字符串

4.拷贝RegExp引用类型会变成空对象

5.对象中含有NaN、Infinity和-Infinity,则序列化的结果会变成null

6.无法拷贝对象的循环应用(即obj[key] = obj)


总结:它会抛弃对象的constructor,深拷贝之后,不管这个对象原来的构造函数是什么,在深拷贝之后都会变成Object;这种方法能正确处理的对象只有 Number, String, Boolean, Array, 扁平对象,也就是说,只有可以转成JSON格式的对象才可以这样用,像function没办法转成JSON;


手动实现深拷贝


这是看github一个栗子,https://github.com/wengjq/Blo...


//类型判断
(function($) {
    "use strict";
    var types = "Array,Object,String,Date,RegExp,Function,Boolean,Number,Null,Undefined".split(",");
    for (let i = types.length; i--; ) {
        $["is" + types[i]] = str => Object.prototype.toString.call(str).slice(8, -1) === types[i];
    }
    return $;
})(window.$ || (window.$ = {})); 
function copy(obj, deep = false, hash = new WeakMap()) {
    if (hash.has(obj)) {
        return hash.get(obj);
    }
    if ($.isFunction(obj)) {
        return new Function("return " + obj.toString())();
    } else if (obj === null || typeof obj !== "object") {
        return obj;
    } else {
        var name,
        target = $.isArray(obj) ? [] : {},
        value;
        hash.set(obj, target);
        for (name in obj) {
            value = obj[name];
            if (deep) {
                if ($.isArray(value) || $.isObject(value)) {
                    target[name] = copy(value, deep, hash);
                } else if ($.isFunction(value)) {
                    target[name] = new Function("return " + value.toString())();
                } else {
                    target[name] = value;
                }
            } else {
                target[name] = value;
            }
        }
      return target;
    }
}
//使用
var x = {};
var y = {};
x.i = y;
y.i = x;
var z = copy(x, true);
console.log(x, z);
var a = {};
a.a = a;
var b = copy(a, true);
console.log(a, b);


第三方深拷贝库


lodash


该函数库也有提供_.cloneDeep用来做 Deep Copy(lodash是一个不错的第三方开源库,有好多不错的函数,也可以看具体的实现源码)


var _ = require('lodash');
var obj1 = {
    a: 1,
    b: { f: { g: 1 } },
    c: [1, 2, 3]
};
var obj2 = _.cloneDeep(obj1);
console.log(obj1.b.f === obj2.b.f); // false


jQuery


jquery 提供一个$.extend可以用来做深拷贝


语法:$.extend([deep], target, object1[, objectN] )


deep:表示是否深拷贝 默认为false 为true为深拷贝,为false,则为浅拷贝

target: Object类型 目标对象,其他对象的成员属性将被附加到该对象上。


object1  objectN: 可选 Object类型 第一个以及第N个被合并的对象。


var $ = require('jquery');
var obj1 = {
   a: 1,
   b: {
     f: {
       g: 1
     }
   },
   c: [1, 2, 3]
};
var obj2 = $.extend(true, {}, obj1);
console.log(obj1.b.f === obj2.b.f);  // false


总结


image.png

相关文章
|
20天前
|
JavaScript 前端开发
如何在 JavaScript 中使用 __proto__ 实现对象的继承?
使用`__proto__`实现对象继承时需要注意原型链的完整性和属性方法的正确继承,避免出现意外的行为和错误。同时,在现代JavaScript中,也可以使用`class`和`extends`关键字来实现更简洁和直观的继承语法,但理解基于`__proto__`的继承方式对于深入理解JavaScript的面向对象编程和原型链机制仍然具有重要意义。
|
24天前
|
Web App开发 JavaScript 前端开发
如何确保 Math 对象的方法在不同的 JavaScript 环境中具有一致的精度?
【10月更文挑战第29天】通过遵循标准和最佳实践、采用固定精度计算、进行全面的测试与验证、避免隐式类型转换以及持续关注和更新等方法,可以在很大程度上确保Math对象的方法在不同的JavaScript环境中具有一致的精度,从而提高代码的可靠性和可移植性。
|
24天前
|
JavaScript 前端开发 图形学
JavaScript 中 Math 对象常用方法
【10月更文挑战第29天】JavaScript中的Math对象提供了丰富多样的数学方法,涵盖了基本数学运算、幂运算、开方、随机数生成、极值获取以及三角函数等多个方面,为各种数学相关的计算和处理提供了强大的支持,是JavaScript编程中不可或缺的一部分。
|
2月前
|
JavaScript 前端开发
JavaScript中的深拷贝与浅拷贝
JavaScript中的深拷贝与浅拷贝
49 4
|
2月前
|
存储 JavaScript 前端开发
JavaScript 对象的概念
JavaScript 对象的概念
38 4
|
2月前
|
缓存 JavaScript 前端开发
JavaScript中数组、对象等循环遍历的常用方法介绍(二)
JavaScript中数组、对象等循环遍历的常用方法介绍(二)
40 1
|
2月前
|
存储 JavaScript 前端开发
js中函数、方法、对象的区别
js中函数、方法、对象的区别
20 2
|
2月前
|
JavaScript 前端开发 Unix
Node.js 全局对象
10月更文挑战第5天
30 2
|
2月前
|
JavaScript 前端开发 大数据
在JavaScript中,Object.assign()方法或展开语法(...)来合并对象,Object.freeze()方法来冻结对象,防止对象被修改
在JavaScript中,Object.assign()方法或展开语法(...)来合并对象,Object.freeze()方法来冻结对象,防止对象被修改
30 0
|
2月前
|
JavaScript 前端开发 索引
JavaScript中数组、对象等循环遍历的常用方法介绍(一)
JavaScript中数组、对象等循环遍历的常用方法介绍(一)
26 0