本文将介绍 JS 的深拷贝和浅拷贝的有关内容。
1. 基本概念
(1)深拷贝和浅拷贝针对的是引用类型。基本类型的名值存储在栈中,当复制时,栈内存会开辟一个栈内存。所以二者修改时,彼此不会影响。
(2)浅拷贝复制的是指向对象的指针,并没有开辟新的栈内存,原对象和新对象还是共享同一块内存,修改新对象自然会影响原对象。
深拷贝会开辟新的栈内存,原对象和新对象不共享同一块内存,修改新对象不会影响到原对象。
2. 实现方式
2.1 浅拷贝的实现方式
2.1.1Object.assign()
可以把任意多个的源对象自身的可枚举属性拷贝给目标对象,然后返回目标对象。
Object.assign()只会拷贝 所有的属性值到新的对象 中,如果属性值是基本类型,则修改其中一个对象,不会影响另一个。而如果属性值是对象的话,拷贝的是对象的引用,而不是对象本身。所以该方法是浅拷贝。
分两种情况:
(1)属性值是基本类型,修改其中一个对象,不会影响另一个对象的该属性:
var iniObj = {
name: "peter",
};
var newObj = Object.assign({}, iniObj);
newObj.name = “lily”;
console.log(‘iniObj.name’, iniObj.name); //peter
(2)属性值是引用类型,由于拷贝的是对象的引用,共享相同的地址:
var iniObj = {
info: {
name: "peter",
age: 8
}
};
var newObj = Object.assign({}, iniObj);
newObj.info.name = “lily”;
console.log(‘iniObj.info.name’, iniObj.info.name); //lily
2.1.2Array.prototype.concat()
concat() 方法用于合并两个或多个数组。此方法不会更改现有数组,而是返回一个新数组。注意这里分两种情况:
(1)对象引用(而不是实际对象):concat 将对象引用复制到新数组中。原始数组和新数组都引用相同的对象。也就是说,如果引用的对象被修改,则更改对于新数组和原始数组都是可见的。这包括也是数组的数组参数的元素。
(2)数据类型如字符串,数字和布尔(不是 String,Number 和 Boolean 对象):concat 将字符串和数字的值复制到新数组中。
示例:
var iniArr = [1,2,{name:”peter”}];
var newArr = iniArr.concat();
newArr[1] = 8;
newArr[2].name = “lily”;
console.log(‘iniArr’,iniArr);
结果:
2.1.3Array.prototype.slice()
slice()方法返回一个新的数组对象,这一对象是一个由 begin 和 end(不包括 end)决定的原数组的浅拷贝。原始数组不会被改变。
也分两种情况:
(1)如果该元素是个对象引用(不是实际的对象),slice 会拷贝这个对象引用到新的数组里。两个对象引用都引用了同一个对象。如果被引用的对象发生改变,则新的和原来的数组中的这个元素也会发生改变。
(2)对于字符串、数字及布尔值来说(不是 String、Number 或者 Boolean 对象),slice 会拷贝些值到新的数组里。在别的数组里修改这些字符串或数字或是布尔值,将不会影响另一个数组。
示例:
var iniArr = [1,2,{name:”peter”}];
var newArr = iniArr.slice();
newArr[1] = 8;
newArr[2].name = “lily”;
console.log(‘iniArr’,iniArr);
结果:
和上述 concat()一样,修改新数组的对象会影响到原数组,修改新数组的基本类型值不会影响到原数组:
2.1.4 引用复制
示例:
var iniObj \= {
gender: "male",
info: {
name: "peter",
age: 8
},
test: \[1, \[2, 3\]\]
};
function shallowCopy(copyObj) {var obj \= {};
for (var i in copyObj) {obj\[i\] \= copyObj\[i\];
}
return obj;
}
var newObj \= shallowCopy(iniObj);
newObj.gender \= "female";
newObj.info.name \= "lily";
newObj.test\[1\] \= \[4, 5\];
console.log('iniObj', iniObj);
console.log('newObj', newObj)
结果:
2.2 深拷贝的实现方式
2.2.1JSON.parse(JSON.stringify())
说明:
1)用 JSON.stringify 将对象转成 JSON 字符串,再用 JSON.parse()把字符串解析成对象。一去一来,新的对象产生了,而且对象 会开辟新的栈,实现深拷贝。
2)这种方法虽然可以实现数组或对象的深拷贝,但不能处理函数。因为 JSON.stringify() 方法是将一个 JavaScript 值 (对象或者数组) 转换为一个 JSON 字符串,不能接受函数
示例:
var iniArr = [1,2,{name:”peter”},function(){}];
var newArr = JSON.parse(JSON.stringify(iniArr));
newArr[1] = 8;
newArr[2].name = “lily”;
console.log(‘JSON.parse(Json.stringfiy()):iniArr,newArr’,iniArr,newArr);
结果:
2.2.2 递归方法
原理:
遍历对象、数组,直到里边都是基本数据类型,然后再去复制,即可实现深度拷贝。
代码:
var iniObj \= {
a: 1,
b: \[1, 2, 3\],
c: {
d: {e: 4}
},
fun: function(){return 999;}
};
function deepClone(x) {if (x.constructor \=== Object) {var obj \= {}
for (var k in x) {obj\[k\] \= deepClone(x\[k\])
}
return obj
} else if (x.constructor \=== Array) {var arr \= \[\];
for (var i \= 0; i < x.length; i++) {arr\[i\] \= deepClone(x\[i\])
}
return arr
} else {return x}
}
var newObj \= deepClone(iniObj)
newObj.b\[0\] \= 6;
newObj.c.d.e \= 8;
console.log('recursive deepClone,iniObj,newObj',iniObj,newObj);
结果:
该方法的局限性:
若属性值是函数、undefined、symbol 时,会忽略掉:
示例:
var iniObj \= {
name:'peter',
age:undefined,
school:function(){}
}
var newObj \= JSON.parse(JSON.stringify(iniObj));
console.log('newObj',newObj)
结果:
尽管有上述局限,不过通常情况下,该函数还是可以解决大部分问题的。
2.2.3jQuery 的 $.extend()
可以用 jQuery 的 $.extend()实现深拷贝:
jQuery.extend([deep], target, object1, [objectN]);
示例:
var iniObj = {
a: 1,
b: \[1, 2, 3\],
c: {
d: {e: 4}
},
};
var newObj = $.extend(true, {}, iniObj);
newObj.b[0] = 6;
newObj.c.d.e = 8;
console.log(‘$.extend deepClone,iniObj,newObj’,iniObj,newObj);
结果:
2.2.4lodash 的_.cloneDeep()
可以用 js 库 lodash 中的_.cloneDeep()实现深拷贝:
示例:
var iniObj \= {
a: 1,
b: \[1, 2, 3\],
c: {
d: {e: 4}
}
};
var newObj \= \_.cloneDeep(iniObj);
newObj.b\[0\] \= 6;
newObj.c.d.e \= 8;
console.log('lodash deepClone,iniObj,newObj',iniObj,newObj);
结果:
3. 赋值
可以和上述 2.1.4 浅拷贝的示例结果做比较,示例:
var iniObj \= {
gender: "male",
info: {
name: "peter",
age: 8
},
test: \[1, \[2, 3\]\]
};
var newObj \= iniObj;
newObj.gender \= "female";
newObj.info.name \= "lily";
newObj.test\[1\] \= \[4, 5\];
console.log('iniObj', iniObj);
console.log('newObj', newObj)
结果:
说明把一个对象赋值给另一个变量时,并没有创建一个新对象,而是把原对象在 栈中的地址(而非栈中的数据)赋给了新对象,即赋的是原对象在栈中的地址,原对象和新对象指向的是同一个地址。因此,两个对象的联动的,修改其中一个,另一个也会改变。包括里面所有的属性,不论是基本类型的数据,还是对象引用。
4. 小结
本篇 JS 基础知识总结,主要介绍了深拷贝和浅拷贝的基本概念,并且分别又介绍了浅拷贝、深拷贝的实现方式,以及与赋值的区别。如有问题,欢迎指正。