JS 深拷貝與淺拷貝


寫在前面: 

  在了解深淺拷貝之前,我們先來了解一下堆棧

  堆棧是一種數據結構,在JS中

  • 棧:由編譯器自動分配釋放 ,存放函數的參數值,局部變量的值等。

      讀寫快,但是存儲的內容較少

  • 堆:一般由程序員分配釋放, 若程序員不釋放,程序結束時可能由OS回收(垃圾回收機制)

        讀寫慢,但是可以存儲較多的內容

     (!!注意:若堆中已動態分配的內存,在使用完之后由於某種原因沒有被釋放或者無法釋放,就會造成系統內存的浪費,導致程序運行速度降低甚至崩潰,這種情況稱為內存泄漏!!)

  JS數據按照在內存中的存儲形式可以分為兩種: 
  • 基本數據類型(存儲在中):string,number,boolean,undefined,null,symbol,以及復雜類型的指針
  • 復雜數據類型(存儲在中):object,array,function 等

 

                棧內存和堆內存

   當我們創建變量並賦值的時候,如果是基本數據類型會直接存儲在棧中,

  而復雜數據類型會存儲在堆中, 當我們將復雜數據類型賦值給某個變量時,只是將該數據在堆中的地址,賦值給了這個變量(指針)。這個變量(指針)存儲了一個指向堆中數據的地址,一般稱之為指針

 

 淺拷貝

  堆棧中數據的拷貝,如果是基本數據類型,那么拷貝的就是數據;

 1         var a = 10;  2         var b = a;  3         console.log('改變前 打印變量 a b');  4         console.log(a);  // 10
 5         console.log(b);  // 10
 6 
 7         var c = 10;  8         var d = c;       // 拷貝的是數據
 9         d = 20;          // 修改拷貝之后的數據
10         console.log("打印變量 c d"); 11         console.log(c);  // 10
12         console.log(d);  // 20

  

 

   如果拷貝的書復雜數據類型,以對象為例,當我們想通過簡單的賦值方式拷貝一個對對象時,

例如:

 1         //復雜數據類型的拷貝
 2         var obj1 = { a: 10 };  3         var obj2 = obj;  4         console.log(obj1);  // { a: 10 }
 5         console.log(obj2); // { a: 10 }
 6         //到這里 我們可以看到obj1和obj2 的打印結果完全相同 也許我們完成了數據的拷貝,
 7         //但是當我們修改拷貝過來的對象的數據時就會出現一個問題
 8         obj2.a = 20;       // 修改拷貝之后的數據
 9         console.log(obj1);  // { a: 20 }
10         console.log(obj2); // { a: 20 } 
11         //此處我們將obj2的a 修改為了20 但是當我們打印這個對象時 , 發現 obj1 中的 a 也被改變了
12         // 思考: 為什么會發生這種情況??

 

   

  分析: 文章開頭關於堆棧的描述中,有提到過當我們新建對象並賦值給一個變量(var obj1 = { a: 10 })的時候,該變量存儲的不是對象的數據,而是該對象在堆中的地址。因此當我們通過這種簡單的方式(obj2 = obj;)拷貝復雜數據類型時,只是拷貝了指針中的地址而已,當你通過原引用修改了對象中的數據,另一個也會感知到這個對象的變化。這種行為被稱為淺拷貝

  復雜數據類型通過普通方式(obj1=obj2)拷貝的是指針,兩個指針引用地址相同,讀取操作的都是同一個數據。  

  一般情況下,等號賦值,函數傳參,都是淺拷貝,也就是只拷貝了數據的地址。

 1         let foo = {title: "hello obj"}  2 
 3         // 等號賦值
 4         let now = foo;  5         now.title = "this is new title";  6 
 7         console.log(foo.title);       // this is new title
 8         console.log(now.title);       // this is new title
 9 
10         // 函數傳參
11         function change(o) { 12             o.title = "this is function change title"; 13  } 14  change(foo); 15         console.log(foo.title);       // this is function change title

 

 

如何實現深拷貝?

  所謂對象的拷貝,其實就是基於復雜數據在拷貝時的異常處理,我們將復雜數據的默認拷貝定義為淺拷貝;就是只拷貝復雜數據的地址,而不拷貝值。那么與之對應的就是不拷貝地址,只拷貝值,也被稱為深拷貝。

1.函數遞歸方式

 1         //代碼分析: 形參obj 代表被拷貝目標, 調用函數 傳入拷貝目標, 
 2         // 通過Array.isArray(obj)判斷obj的類型是否為數組
 3         // 通過 for in 遍歷拷貝目標, 
 4         // 使用 typeof 判斷其每一個元素或者屬性, 是否為obj類型(typeof Array/Object 返回值皆為object)
 5         // 若該屬性/元素, 部位null 並且 typeof返回值為object, 則代表其為復雜數據類型, 遞歸調用 deepCopy(obj[key]),繼續拷貝其內部
 6         // 否則: 代表該元素非 數組 非對象, 為基本數據類型/函數 等 , 直接賦值拷貝即可
 7         // 最后返回拷貝完成的result ,函數執行完畢 
 8         function deepCopy(obj) {  9             var result = Array.isArray(obj) ? [] : {}; 10             for (var key in obj) { 11                 if (typeof obj[key] === 'object' && obj[key] !== null) { 12                     result[key] = deepCopy(obj[key]); //遞歸復制
13                 } else { 14                     result[key] = obj[key]; 15  } 16  } 17             return result; 18         }

 

 

 2.利用JS中對JSON的解析方法

    什么是JSON?   

      JSON( JavaScript Object Notation) 是一種輕量級的存儲和傳輸數據的格式。經常在數據從服務器發送到網頁時使用。

    JavaScript   JSON方法

      JSON.stringify(value)  方法用於將 JavaScript 值轉換為 JSON 字符串,並返回該字符串。

      JSON.parse(value)      用於將一個 JSON 字符串轉換為對象 並返回該對象。

 1         let obj = {  2  title: {  3                 sTitle: 0,
4 list: [1, 2, { a: 3, b: 4 }] 5 } 6 } 7 let obj2 = JSON.parse(JSON.stringify(obj)); 8 //通過JSON的方式對數據進行處理轉換時, 不是改變原數據, 而是在內存中開辟一個新空間來存儲轉換的數據, 9 //這樣兩次轉換后, 返回的數據 ,與原數據內容相同但是存儲地址不同, 不存在引用關系 10 console.log(obj,obj2); 11 // 深拷貝成功 12 console.log(foo === now); // false

 

 

 

 

 

 

 

 

   缺陷 : 受json數據的限制,無法拷貝函數,undefined,NaN屬性

 1         let obj={  2             a:10,  3             b:[1,2,3,{c:10}],  4  d:undefined,  5  e(){  6                 console.log(this.a);  7  },  8  f:NaN  9  } 10         let obj2 = JSON.parse(JSON.stringify(obj)); 11         console.log(obj,obj2);

 

 

 

 

 

 

 3.利用ES6 提供的 Object.assign()    

   只能可以拷貝一層數據,無法拷貝多層數據,內層依然為淺拷貝

 1 let foo = {  2  title:{  3 show:function(){},  4  num:NaN,  5  empty:undefined  6  }  7 }  8  9 let now = {}; 10 11 Object.assign(now, foo); 12 13 console.log(foo); // {title: {{num: NaN, empty: undefined, show: ƒ}}} 14 console.log(now); // {title: {{num: NaN, empty: undefined, show: ƒ}}} 15 16 // 外層對象深拷貝成功 17 console.log(foo === now); // false 18 // 內層對象依然是淺拷貝 19 console.log(foo.title === now.title); // true 

 

 

4. 利用ES6 提供的展開運算符:...

 1 let foo = {  2  title:{  3         show:function(){},  4  num:NaN,  5  empty:undefined  6  }  7 }  8 
 9 let now = {...foo}; 10 
11 console.log(foo);   // {title: {{num: NaN, empty: undefined, show: ƒ}}}
12 console.log(now);   // {title: {{num: NaN, empty: undefined, show: ƒ}}}
13 
14 // 外層對象深拷貝成功
15 console.log(foo === now);                // false
16 // 內層對象依然是淺拷貝
17 console.log(foo.title === now.title);    // true 

 

5.使用函數庫lodash中的cloneDeep()方法

使用方法:

1.下載模塊

1    cnpm i lodash --save 
2    yarn add lodash

 

2.引入模塊

1    import _ from 'lodash'

 

3.使用

1    let obj1 = loodash.cloneDeep(obj) 

 

6. 使用immutable-js

 

 

其他(參考用, 仍可完善)

 1 function cloneObj(source, target) {
 2     // 如果目標對象不存在,根據源對象的類型創建一個目標對象
 3     if (!target) target = new source.constructor();
 4     // 獲取源對象的所有屬性名,包括可枚舉和不可枚舉
 5     var names = Object.getOwnPropertyNames(source);
 6     // 遍歷所有屬性名
 7     for (var i = 0; i < names.length; i++) {
 8         // 根據屬性名獲取對象該屬性的描述對象,描述對象中有configurable,enumerable,writable,value
 9         var desc = Object.getOwnPropertyDescriptor(source, names[i]);
10         // 表述對象的value就是這個屬性的值
11         // 判斷屬性值是否不是對象類型或者是null類型
12         if (typeof desc.value !== "object" || desc.value === null) {
13             // 定義目標對象的屬性名是names[i],值是上面獲取該屬性名的描述對象
14             // 這樣可以將原屬性的特征也復制了,比如原屬性是不可枚舉,不可修改,這里都會定義一樣
15             Object.defineProperty(target, names[i], desc);
16         } else {
17             // 新建一個t對象
18             var t = {};
19             // desc.value 就是源對象該屬性的值
20             // 判斷這個值是什么類型,根據類型創建新對象
21             switch (desc.value.constructor) {
22                 // 如果這個類型是數組,創建一個空數組
23                 case Array:
24                     t = [];
25                     break;
26                 // 如果這個類型是正則表達式,則將原值中正則表達式的source和flags設置進來
27                 // 這兩個屬性分別對應正則desc.value.source 正則內容,desc.value.flags對應修飾符
28                 case RegExp:
29                     t = new RegExp(desc.value.source, desc.value.flags);
30                     break;
31                 // 如果是日期類型,創建日期類型,並且把日期值設置相同
32                 case Date:
33                     t = new Date(desc.value);
34                     break;
35                 default:
36                     // 如果這個值是屬於HTML標簽,根據這個值的nodeName創建該元素
37                     if (desc.value instanceof HTMLElement)
38                         t = document.createElement(desc.value.nodeName);
39                     break;
40             }
41             // 將目標元素,設置屬性名是names[i],設置value是當前創建的這個對象
42             Object.defineProperty(target, names[i], {
43                 enumerable: desc.enumerable,
44                 writable: desc.writable,
45                 configurable: desc.configurable,
46                 value: t
47             });
48             // 遞歸調用該方法將當前對象的值作為源對象,將剛才創建的t作為目標對象
49             cloneObj(desc.value, t);
50         }
51     }
52     return target;
53 }

 


免責聲明!

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



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