JS中如何進行對象的深拷貝


在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的對象深拷貝方法

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;  
}; 

這里有三點需要注意:

1、用new obj.constructor ()構造函數新建一個空的對象,而不是使用{}或者[],這樣可以保持原形鏈的繼承;
2、用obj.hasOwnProperty(key)來判斷屬性是否來自原型鏈上,因為for..in..也會遍歷其原型鏈上的可枚舉屬性。
3、上面的函數用到遞歸算法,在函數有名字,而且名字以后也不會變的情況下,這樣定義沒有問題。但問題是這個函數的執行與函數名 factorial 緊緊耦合在了一起。為了消除這種緊密耦合的現象,需要使用 arguments.callee

四、面試惡心人的問題:

  寫一個能拷貝自身可枚舉、自身不可枚舉、自身 Symbol 類型鍵、原型上可枚舉、原型上不可枚舉、原型上的 Symol 類型鍵,循環引用也可以拷的深拷貝函數:

function cloneDeep(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);

    let family = {}
    let parent = Object.getPrototypeOf(obj)

    while (parent != null) {
        family = completeAssign(deepClone(family), parent)
        parent = Object.getPrototypeOf(parent)
    }

    // 下面這個函數會拷貝所有自有屬性的屬性描述符
    function completeAssign(target, ...sources) {
        sources.forEach(source => {
            let descriptors = Object.keys(source).reduce((descriptors, key) => {
                descriptors[key] = Object.getOwnPropertyDescriptor(source, key)
                return descriptors
            }, {})

            // Object.assign 默認也會拷貝可枚舉的Symbols
            Object.getOwnPropertySymbols(source).forEach(sym => {
                let descriptor = Object.getOwnPropertyDescriptor(source, sym)
                if (descriptor.enumerable) {
                    descriptors[sym] = descriptor
                }
            })
            Object.defineProperties(target, descriptors)
        })
        return target
    }

    return completeAssign(deepClone(obj), family)
}

提示的Object.getOwnPropertyDescriptors中的value是淺拷貝,那么一旦對象中出現了日期、正則這一類特殊對象,就會出現拷貝丟失;所以開頭針對特殊對象,需要特殊對待。

 

 

參考:
1、https://blog.csdn.net/qq_34629352/article/details/105375985

2、https://blog.csdn.net/ih1107/article/details/79208860


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM