今天和大家一起分享在JavaScript中如何實現深拷貝。
0. 為什么要實現深拷貝
在之前的一篇文章中 JavaScript變量存儲淺析(二) 我們已經知道,在JS中,如果只是將一個對象簡單的賦值給另外一個對象,那么拷貝的實際上只是對象在堆內存中的地址而已,也就是說,拷貝后的對象仍然和源對象指向同一個內存中的對象,只是修改其中一個對象,那么另外一個對象也會隨之被修改。
我們通過一個簡單的例子來解釋這個問題:
1 var obj1={ 2 attr:100 3 }; 4 var obj2=obj1; //簡單復制obj1對象 5 obj2.attr=200; //修改obj2屬性 6 console.log(obj1.attr); //輸出200,說明obj1也被修改
OK,這就是深拷貝的意義所在。完整的拷貝一個對象的所有屬性,而不是引用地址。
數組是這樣的情況嗎?大家可以自行驗證一下!
1. 數據類型判斷
實現深拷貝的第一步就是判斷數據類型。
我們都知道,JavaScript的數據類型分為兩大類:
- 基本類型:String,Number,Boolean,undefined,Null
- 引用類型:Object,Array,Date,Reg,Function等
對於基本類型的判斷,我們使用typeof就可以,對於實例類型,也可以通過instanceof來判斷。
除了這兩個方法以外,我們還有一些別的方式來判斷,就是Object下的toString方法.
偷個懶,從MDN上查詢下該方法的調用:
也就是說,我們只需要截取返回值的type值就可以了。下面是一個參考方法:
1 var util={ 2 getType:function(o){ //判斷對象類型 3 var _t; 4 return ((_t = typeof(o)) == "object" ? o==null && "null" || Object.prototype.toString.call(o).slice(8,-1):_t).toLowerCase(); 5 } 6 };
我們先定義了一個util對象用於存放本節需要使用的相關方法,getType方法用於檢測對象類型。
實現的原理上面也提及了,如果是基本類型的話,就直接返回typeof值。如果是對象類型,我們還需要進行細分,從第8個字符開始截取到倒數第二個字符作為返回值。
這個方法大家可以實際操作和驗證一下。
2. 深拷貝
一般情況下,我們主要解決以下引用類型的深度拷貝:
- 對象:遍歷對象的所有屬性,將其值拷貝到目標元素的對應屬性上。
- 數組:遍歷數組的所有元素,將其值分別拷貝到目標數組的對象index下。
- 函數:一般來說不作特殊處理,如果需要的話可以先將function通過tostring方法轉換為字符串,然后再調用eval還原函數。
本文的重點在於對象與數組的深度拷貝上。
我們在util對象上添加了deepClone方法用於實現深拷貝,需要傳入源對象作為參數。
1 var util={ 2 getType:function(o){ //判斷對象類型 3 var _t; 4 return ((_t = typeof(o)) == "object" ? o==null && "null" || Object.prototype.toString.call(o).slice(8,-1):_t).toLowerCase(); 5 }, 6 deepClone:function(source){ //深拷貝 7 var self=this; //保存當前對象引用 8 var destination=self.getType(source); 9 destination=destination==='array'?[]:(destination==='object'?{}:source); 10 for (var p in source) { 11 if (self.getType(source[p]) === "array" || self.getType(source[p]) === "object") { 12 destination[p] = self.getType(source[p]) === "array" ? [] : {}; 13 destination[p]=arguments.callee.call(self, source[p]); //使用call修改函數的作用域 14 } else { 15 destination[p] = source[p]; 16 } 17 } 18 return destination; 19 } 20 };
- 第7行:保存當前對象的引用,便於后面做遞歸調用的時候修改作用域。
- 第8行:拿到source源對象的類型。
- 第9行:如果類型為數組的話,我們就創建一個空數組;如果是對象的話,創建一個空對象;然后將創建的空對象或空數組賦值到destination這個局部變量上。如果不是這兩種類型的話,那就不屬於深拷貝的范圍,我們直接將源對象的值賦值回去。
- 第10-11行:使用for..in對源對象進行循環,遍歷其所有元素或屬性。
- 第12行:同樣的,根據每個屬性值的類型,在destination創建一個對應的空對象或空數組。
- 第13行:使用callee進行函數的遞歸調用,再次計算每個屬性或元素的值。
2015-12-24 更新:這里增加了call函數來遞歸調用當前函數的引用,並且修改了其作用域。
特別鳴謝:感謝 rookieCat 指出了使用callee 時的作用域問題。
- 第18行:返回局部變量destination。
整個的實現核心就在於我們要清楚需要處理哪些類型的數據,以及使用callee進行遞歸調用。
好的,下面我們來使用深拷貝方法,看能否達到想要的效果:
1 var obj1={ 2 attr:100 3 }; 4 5 6 var obj2=util.deepClone(obj1); //將obj1深拷貝到obj2 7 obj2.attr=200; //修改obj2的屬性值 8 console.log(obj1.attr); //obj1屬性值未發生變化
最終的結果是:通過深拷貝得到的新對象在內存中有獨立的存儲位置,因此修改新對象不會對源對象造成任何影響。