寫在前面:
在了解深淺拷貝之前,我們先來了解一下堆棧。
堆棧是一種數據結構,在JS中
- 棧:由編譯器自動分配釋放 ,存放函數的參數值,局部變量的值等。
讀寫快,但是存儲的內容較少
- 堆:一般由程序員分配釋放, 若程序員不釋放,程序結束時可能由OS回收(垃圾回收機制)
讀寫慢,但是可以存儲較多的內容
(!!注意:若堆中已動態分配的內存,在使用完之后由於某種原因沒有被釋放或者無法釋放,就會造成系統內存的浪費,導致程序運行速度降低甚至崩潰,這種情況稱為內存泄漏!!)
- 基本數據類型(存儲在棧中):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 }