深入 JS 之深浅拷贝
细说赋值与浅拷贝的区别
当我们把一个对象赋值给一个新的变量时,赋的其实是该对象的在栈中的地址,而不是堆中的数据。也就是两个对象指向的是同一个存储空间,无论哪个对象发生改变,其实都是改变的存储空间的内容,因此,两个对象是联动的。
浅拷贝是按位拷贝对象,它会创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值;如果属性是内存地址(引用类型),拷贝的就是内存地址 ,因此如果其中一个对象改变了这个地址,就会影响到另一个对象。即默认拷贝构造函数只是对对象进行浅拷贝复制(逐个成员依次拷贝),即只复制对象空间而不复制资源。
我们先来看两个例子,对比赋值与浅拷贝会对原对象带来哪些改变?
// 对象赋值 var obj1 = { name: "zhangsan", age: "18", language: [1, [2, 3], [4, 5]] }; var obj2 = obj1; obj2.name = "lisi"; obj2.language[1] = ["二", "三"]; console.log("obj1", obj1); console.log("obj2", obj2);
// 浅拷贝 var obj1 = { name: "zhangsan", age: "18", language: [1, [2, 3], [4, 5]] }; var obj3 = shallowCopy(obj1); obj3.name = "lisi"; obj3.language[1] = ["二", "三"]; function shallowCopy(src) { var dst = {}; for (var prop in src) { if (src.hasOwnProperty(prop)) { dst[prop] = src[prop]; } } return dst; } console.log("obj1", obj1); console.log("obj3", obj3);
上面例子中,obj1 是原始数据,obj2 是赋值操作得到,而 obj3 浅拷贝得到。我们可以很清晰看到对原始数据的影响。
浅拷贝的实现方式
1. Object.assign()
Object.assign() 方法可以把任意多个的源对象自身的可枚举属性拷贝给目标对象,然后返回目标对象。但是 Object.assign() 进行的是浅拷贝,拷贝的是对象的属性的引用,而不是对象本身。
var obj = { a: { a: "kobe", b: 39 } }; var initalObj = Object.assign({}, obj); initalObj.a.a = "wade"; console.log(obj.a.a); //wade
注意:当 object 只有一层的时候,是深拷贝。
let obj = { username: "kobe" }; let obj2 = Object.assign({}, obj); obj2.username = "wade"; console.log(obj); //{username: "kobe"}
2. Array.prototype.slice()
let arr = [ 1, 3, { username: " kobe" } ]; let arr3 = arr.slice(); arr3[2].username = "wade"; console.log(arr);
3. Array.prototype.concat()
let arr = [ 1, 3, { username: "kobe" } ]; let arr2 = arr.concat(); arr2[2].username = "wade"; console.log(arr);
关于 Array 的 slice 和 concat 方法的补充说明:Array 的 slice 和 concat 方法不修改原数组,只会返回一个浅复制了原数组中的元素的一个新数组。
4. 扩展运算符
let a = { name: "sadhu", book: { title: "You Don't Know JS", price: "45" } } let b = {...a}; console.log(b); // { // name: "sadhu", // book: {title: "You Don't Know JS", price: "45"} // } a.name = "change"; a.book.price = "55"; console.log(a); // { // name: "change", // book: {title: "You Don't Know JS", price: "55"} // } console.log(b); // { // name: "sadhu", // book: {title: "You Don't Know JS", price: "55"} // }
手写一个简易浅拷贝
遍历对象,然后把属性和属性值都放在一个新的对象就好了。
var shallowCopy = function(obj) { // 只拷贝对象 if (typeof obj !== 'object') return; // 根据obj的类型判断是新建一个数组还是对象 var newObj = obj instanceof Array ? [] : {}; // 遍历obj,并且判断是obj的属性才拷贝 for (var key in obj) { if (obj.hasOwnProperty(key)) { newObj[key] = obj[key]; } } return newObj; }
关于思维导图中深拷贝的一点补充
就补充一个关于 JSON.parse(JSON.stringify()) 例子:
let obj = { reg: /^asd\$/, fun: function() {}, und: undefined, syb: Symbol("foo"), asd: "asd" }; let cp = JSON.parse(JSON.stringify(obj)); console.log(cp);
可以看到,函数、正则、Symbol、undefined 都没有被正确的复制。
手写一个简易深拷贝
我们在浅拷贝的基础上判断一下属性值的类型,如果是对象,我们递归调用深拷贝函数:
var deepCopy = function(obj) { if (typeof obj !== 'object') return; var newObj = obj instanceof Array ? [] : {}; for (var key in obj) { if (obj.hasOwnProperty(key)) { newObj[key] = typeof obj[key] === 'object' ? deepCopy(obj[key]) : obj[key]; } } return newObj; }
这只是简易,针对普通应用场景。有问题比如 typeof
判断返回的是 function
,这种函数的复制是一个很难解决的问题呀,即使是 jQuery 的 extend 也没有去处理函数。关于null和循环引用也没有考虑,需要用到时可以去看看我后面贴出的参考文章里的代码。
性能问题
尽管使用深拷贝会完全的克隆一个新对象,不会产生副作用,但是深拷贝因为使用递归,性能会不如浅拷贝,在开发中,还是要根据实际情况进行选择。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论