JS中如何进行对象的深拷贝

时间: 2018-11-08阅读: 37标签: 拷贝

在JS中,一般的=号传递的都是对象/数组的引用,并没有真正地拷贝一个对象,那如何进行对象的深度拷贝呢?如果你对此也有疑问,这篇文章或许能够帮助到你。

对象的深拷贝与浅拷贝的区别如下:浅拷贝:仅仅复制对象的引用,而不是对象本身;深拷贝:把复制的对象所引用的全部对象都复制一遍。


一、对象引用、浅层拷贝与深层拷贝的区别

js的对象引用传递理解起来很简单,参考如下代码:

var a = {name:'wanger'}
var b = a ;
a===b // true
b.name = 'zhangsan'
a.name //'zhangan'

上述代码中,使用了=进行赋值,于是b指向了a所指向的栈的对象,也就是a与b指向了同一个栈对象,所以在对b.name赋值时,a.name也发生了变化。为了避免上面的情况,可以对对象进行拷贝,代码如下:

var a = {name:'wanger'}
var b = Object.assign({}, a)
a===b // false
b.name = 'zhangsan'
a.name //'wanger'

上面代码将原始对象拷贝到一个空对象,就得到了原始对象的克隆,这时候a与b指向的是不同的栈对象,所以对b.name重新复制也不会影响到a.name。但是如果a.name是一个对象的引用,而不是一个字符串,那么上面的代码也会遇到一些问题,参考如下代码:

var a = {name:{firstName:'wang',lastName:'er'}}
var b = Object.assign({}, a)
a===b // false
b.name.firstName = 'zhang'
a.name.firstName //'zhang'

b.name.firstName又影响到了a.name.firstName,这是因为Object.assign()方法只是浅层拷贝,a.name是一个栈对象的引用,赋值给b时,b.name也同样是这个栈对象的引用,很多时候,我们不想让这种事情发生,所以我们就需要用到对象的深拷贝。


二、使用JSON.parse()与JSON.stringify()对对象进行拷贝

通常情况下,我们可以使用JSON.parse()与 JSON.stringify()实现对象的深克隆,如下:

var clone = function (obj) {
    return JSON.parse(JSON.stringify(obj));
}

这种方法只适用于纯数据json对象的深度克隆,因为有些时候,这种方法也有缺陷,参考如下代码:

var clone = function (obj) {
    return JSON.parse(JSON.stringify(obj));
}
var a = {a:function(){console.log('hello world')},b:{c:1},c:[1,2,3],d:"wanger",e:new Date(),f:null,g:undefined}
var b = clone(a)

打印如下:

我们发现,上述的方法会忽略值为function以及undefied的字段,而且对date类型的支持也不太友好。

更要紧的是,上述方法只能克隆原始对象自身的值,不能克隆它继承的值,参考如下代码:

function Person (name) {
    this.name = name
}
var wanger = new Person('王二')
var newwanger = clone(wanger)
wanger.constructor === Person // true
newwanger.constructor === Object // true

打印如下:

我们发现,克隆的对象的构造函数已经变成了Object,而原来的对象的构造是Person。


三、目前没有发现bug的对象深拷贝方法

王二在网上参考了不少文章,方法都不尽完美,于是在前人基础上改造了一下,方法如下,目前没有发现有什么bug:

var clone = function (obj) { 
    if(obj === null) return null 
    if(typeof obj !== 'object') return obj;
    if(obj.constructor===Date) return new Date(obj); 
    var newObj = new obj.constructor ();  //保持继承链
    for (var key in obj) {
        if (obj.hasOwnProperty(key)) {   //不遍历其原型链上的属性
            var val = obj[key];
            newObj[key] = typeof val === 'object' ? arguments.callee(val) : val; // 使用arguments.callee解除与函数名的耦合
        }
    }  
    return newObj;  
};

这里有三点需要注意:
1、用new obj.constructor ()构造函数新建一个空的对象,而不是使用{}或者[],这样可以保持原形链的继承;
2、用obj.hasOwnProperty(key)来判断属性是否来自原型链上,因为for..in..也会遍历其原型链上的可枚举属性。
3、上面的函数用到递归算法,在函数有名字,而且名字以后也不会变的情况下,这样定义没有问题。但问题是这个函数的执行与函数名 factorial 紧紧耦合在了一起。为了消除这种紧密耦合的现象,需要使用 arguments.callee。
4、以上方法并没有对 function 做拷贝。



2017-10-03添加,之前没有考虑正则对象的问题,这里做一下修改:

var clone = function (obj) { 
    if(obj === null) return null 
    if(typeof obj !== 'object') return obj;
    if(obj.constructor===Date) return new Date(obj); 
    if(obj.constructor === RegExp) return new RegExp(obj);
    var newObj = new obj.constructor ();  //保持继承链
    for (var key in obj) {
        if (obj.hasOwnProperty(key)) {   //不遍历其原型链上的属性
            var val = obj[key];
            newObj[key] = typeof val === 'object' ? arguments.callee(val) : val; // 使用arguments.callee解除与函数名的耦合
        }
    }  
    return newObj;  
};



2018-08-24添加,还可以使用 MessageChannel实现深克隆:

如果所需拷贝的对象含有内置类型并且不包含函数,可以使用 MessageChannel,参考如下代码:

function structuralClone(obj) {
    return new Promise(resolve => {
        const {port1, port2} = new MessageChannel();
        port2.onmessage = ev => resolve(ev.data);
        port1.postMessage(obj);
    });
}
var obj = {a: 1, b: {
    c: b
}}
// 注意该方法是异步的
// 可以处理 undefined 和循环引用对象
var clone = await structuralClone(obj);

此方法参考自InterviewMap;


原文地址:王玉略的个人网站