往期笔记实例引入
- 在ES6的解构赋值中我们提到过:使用扩展运算符复制对象是浅拷贝的。
- 在Redux入门(1)中我们提到过:不能修改state,所以我们需要对state进行深拷贝成newState。
基本类型与引用类型
- 数据分为基本数据类型(String, Number, Boolean, Null, Undefined,Symbol)和引用数据类型( Object,Array,Date,Function)。
- 基本类型:名字和值都会储存在栈内存中。
- 引用类型:名字存在 栈内存 中,值存在 堆内存 中,但是 栈内存 会提供一个 引用的地址 指向堆内存中的值(指针)。
赋值和浅拷贝的区别
- 当我们把一个对象赋值给一个新的变量时,赋的其实是该对象的在栈中的地址,而不是堆中的数据。也就是两个对象指向的是同一个存储空间,无论哪个对象发生改变,其实都是改变的存储空间的内容,因此,两个对象是联动的。
- 浅拷贝是按位拷贝对象,它会创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。
- 如果属性是基本类型,拷贝的就是基本类型的值.
- 如果属性是引用类型,拷贝的就是内存地址.
- 因此如果其中一个对象改变了这个地址,就会影响到另一个对象。即默认拷贝构造函数只是对对象进行浅拷贝复制(逐个成员依次拷贝),即只复制对象空间而不复制资源。
例子
对比赋值与浅拷贝会对原对象带来哪些改变:
1 | // 对象赋值 |
1 | // 浅拷贝 |
上面例子中,obj1是原始数据,obj2是赋值操作得到,而obj3浅拷贝得到。我们可以很清晰看到对原始数据的影响,具体请看下表:
深拷贝与浅拷贝
假设B复制了A,当修改A时,看B是否会发生变化,如果B也跟着变了,说明这是浅拷贝,如果B没变,那就是深拷贝。
- 浅拷贝只复制指向某个对象的指针,而不复制对象本身,新旧对象还是共享同一块内存。
- 浅拷贝:拷贝一个变量的时候,复制了栈内存,没有复制 堆内存。
- 但深拷贝会另外创造一个一模一样的对象,新对象跟原对象不共享内存,修改新对象不会改到原对象。
- 深拷贝:拷贝一个变量的时候,复制了栈内存,同时也复制了 堆内存。
深拷贝实现思路
在 堆内存 中也开辟一个新的内存专门为b存放值,就像基本类型那样,也就达到深拷贝的效果了。
浅拷贝:
浅拷贝可以理解为就是复制一份来引用,所有引用对象都指向一份数据,并且都可以修改这份数据。 对于字符串类型,浅拷贝是对值的拷贝,对于对象来说,浅拷贝是对对象地址 的拷贝,也就是复制的结果是两个对象指向同一个内存地址,修改其中一个对象的属性,则另一个对象的属性也会改变。
深拷贝:
深拷贝则是复制变量值,对于非基本类型的变量,则递归至基本类型变量后,再复制。深复制不同于浅复制,它会开辟新的内存地址,两个对象对应两个不同的地址,修改一个对象的属性,不会改变另一个对象的属性。
深、浅拷贝的优缺点
- 浅拷贝
- 优点:拷贝速度快,占用内存空间小。
- 缺点:如果你改变了对象B所指向的内存地址,你同时也会改变对象A指向这个地址的字段。
- 深拷贝
- 这种方式会完全拷贝所有数据,优点是B与A不会相互依赖(A,B完全脱离关联)
- 缺点:拷贝的速度更慢,代价更大 (可理解为耗费了更多内存空间)。
对象的浅拷贝
Object.assign()
- 语法:
Object.assign(目标对象, ...源对象)
- Object.assign() 方法可以把任意多个的源对象自身的可枚举属性拷贝给目标对象,然后返回目标对象。
- 但是 Object.assign() 进行的是浅拷贝,拷贝的是对象的属性的引用,而不是对象本身。
- 注意:当object只有一层的时候,是深拷贝
- MDN文档
1
2
3
4
5// 浅拷贝
var obj = { a: {a: "kobe", b: 39} };
var initalObj = Object.assign({}, obj);
initalObj.a.a = "wade";
console.log(obj.a.a); // wade
特别情况深拷贝
注意:当object只有一层的时候,是深拷贝
1 | // 当object只有一层的时候,是深拷贝 |
数组的浅拷贝
slice和concat这两个方法,仅适用于对不包含引用对象的一维数组的深拷贝。对于包含引用对象的数组,这两个方法是浅拷贝(比如[{"name":"weifeng"},{"name":"boy"}]
)
Array.prototype.slice方法
- 对于array对象的slice函数,返回一个数组的一段。(仍为数组)
- 语法:
arrayObj.slice(start, [end])
- arrayObj 必选项。一个 Array 对象。
- start 必选项。arrayObj 中所指定的部分的开始元素是从零开始计算的下标。
- end 可选项。arrayObj 中所指定的部分的结束元素是从零开始计算的下标。
- 说明:
- slice 方法返回一个 Array 对象,其中包含了 arrayObj 的指定部分。
- slice 方法一直复制到 end 所指定的元素,但是不包括该元素。
- 如果 start 为负,将它作为 length + start处理,此处 length 为数组的长度。
- 如果 end 为负,就将它作为 length + end 处理,此处 length 为数组的长度。
- 如果省略 end ,那么 slice 方法将一直复制到 arrayObj 的结尾。
- 如果 end 出现在 start 之前,不复制任何元素到新数组中。
深拷贝与浅拷贝
- 当数组元素是基本数据类型,比如String,Number,Boolean时,属于深拷贝
- 当数组元素是引用数据类型,比如Object,Array,Function时,属于浅拷贝
1
2
3
4
5var arr1 = ["1","2","3"];
var arr2 = arr1.slice(0);
arr2[1] = "9";
console.log("数组的原始值:" + arr1 );//1 2 3
console.log("数组的新值:" + arr2 );//1 9 3
Array.prototype.concat方法
- concat() 方法用于连接两个或多个数组。该方法不会改变现有的数组,而仅仅会返回被连接数组的一个副本。
- 语法:
arrayObject.concat(arrayX,arrayX,......,arrayX)
- 说明:返回一个新的数组。该数组是通过把所有 arrayX 参数添加到 arrayObject 中生成的。如果要进行 concat() 操作的参数是数组,那么添加的是数组中的元素,而不是数组。
深拷贝与浅拷贝
- 当数组元素是基本数据类型,比如String,Number,Boolean时,属于深拷贝
1
2
3
4
5var arr1 = ["1","2","3"];
var arr2 = arr1.concat();
arr2[1] = "9";
console.log("数组的原始值:" + arr1 );// 1 2 3
console.log("数组的新值:" + arr2 );//1 9 3 - 当数组元素是引用数据类型,比如Object,Array时,属于浅拷贝
1
2
3
4
5var arr1 = [{a:1},{b:2},{c:3}];
var arr2 = arr1.concat();
arr2[0].a = "9";
console.log("数组的原始值:" + arr1[0].a ); // 数组的原始值:9
console.log("数组的新值:" + arr2[0].a ); // 数组的新值:9
二维数组/对象 通用深拷贝方法
递归拷贝所有层级属性
- 原理:遍历对象、数组直到里边都是基本数据类型,然后再去复制,就是深度拷贝。
- 复习:
typeof
将引用类型(数组、对象)都判断为object
。- (更多
for-in
与for-of
可参考《ES6扩展 对象扩展》)- 深拷贝不使用for-of的原因: 数组元素/对象属性为不可迭代的对象时会报错。
for-in
主要用于对 对象属性 进行遍历。for-in
循环中的代码每执行一次,就会对 对象的属性 进行一次操作。for(key in obj)
中,key是属性名。(注意:in操作符针对数组时,key是数组下标)- 对象/数组/字符串 都有属性继承自原型。使用
for-in
遍历时会遍历到继承属性,所以需要提前筛选。Object的hasOwnProperty()方法返回一个布尔值,判断对象是否包含特定的自身(非继承)属性。(对数组使用for-of
时不需判断,例子可看MDN,注意数组也是会有继承属性的)
for-of
主要用于对 数组元素 进行遍历,不使用for-in
是因为for-in
无法保证 索引顺序 ,所以使用for-of
或者forEach()
遍历数组会更加合理。- 不使用for-of的原因: 数组元素/对象属性为不可迭代的对象时会报错。
for(key of arr)
中,key是数组元素。(不同于for(key in arr)
中key是数组下标)
1 | /** |
1 | /** |
1 | /** |
JSON.parse(JSON.stringify())
- 这也是我在Redux入门(1)中对state进行深拷贝成newState时使用的方法。(由于这里的state对象中没有方法,所以可以使用JSON进行深拷贝)
- 原理:不拷贝引用对象,使用
stringify()
拷贝一个 JSON字符串 会新辟一个新的存储地址,这样就切断了引用对象的指针联系。 - 缺点:无法实现对 数组/对象中 方法 的深拷贝,会显示为undefined。
- 如果是对象第一层属性中含有方法,则可以使用ES6提供的
object.assign()
进行对象方法的拷贝- 注意:仅限对象第一层属性的方法,因为assign是浅拷贝的,第二层属性的方法就只拷贝引用地址了。
- assign处理数组的时候,会把数组视为对象,返回的是对象
- 如果是对象第一层属性中含有方法,则可以使用ES6提供的
使用assign可解决JSON拷贝的缺点,拷贝对象第一层属性中的方法:
1 | let obj1 = { |
assign仅限拷贝对象第一层属性的方法,因为assign是浅拷贝的,第二层属性的方法就只拷贝引用地址了:
1 | let obj1 = { |
例子:
1 | var 新对象 = JSON.parse(JSON.stringify(老对象)); |
stringify()
将 JS对象 转换为 JSON字符串parse()
将 JSON字符串 转换为 JS对象
1 | var target = [0, 1, null, [1, 2], { name: "a" }, function a() { return 1; }] |
函数库lodash
该函数库也有提供 _.cloneDeep
用来做 Deep Copy。
1 | var _ = require('lodash'); |
扩展运算符的深、浅拷贝
- 当value是基本数据类型,比如String,Number,Boolean时,是可以使用拓展运算符进行深拷贝的
- 当value是引用类型的值,比如Object,Array,引用类型进行深拷贝也只是拷贝了引用地址,所以属于浅拷贝
1
2
3
4var car = {brand: "BMW", price: "380000", length: "5米"}
var car1 = { ...car, price: "500000" }
console.log(car1); // { brand: "BMW", price: "500000", length: "5米" }
console.log(car); // { brand: "BMW", price: "380000", length: "5米" }