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