JavaScript深拷貝初探


 

  今天和大家一起分享在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屬性值未發生變化

  最終的結果是:通過深拷貝得到的新對象在內存中有獨立的存儲位置,因此修改新對象不會對源對象造成任何影響。

  

  

  

  


免責聲明!

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



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