深拷貝與淺拷貝
淺拷貝
對於基本類型,淺拷貝是對值的復制,對於對象來說,淺拷貝只復制指向某個對象的指針,而不復制對象本身,並沒有開辟新的棧,也就是復制的結果是新舊對象還是共享同一塊內存,兩個對象指向同一個地址,修改其中一個對象的屬性,則另一個對象的屬性也會改變。
深拷貝
深拷貝會開辟新的棧,創造一個一模一樣的對象,兩個對象對應兩個不同的地址,不共享內存,修改一個對象的屬性,不會改變另一個對象的屬性。
在js中,對象跟基本類型最大的不同就在於他們的傳值方式。
基本類型是傳 value,如下:
var a = 10; var b = a; b = 20; console.log(a); //10 console.log(b); //20
在修改a時並不會改到b。
但對象就不同,對象傳的是reference:
var obj1 = { a: 10, b: 20, c: 30 }; var obj2 = obj1; obj2.b = 100; console.log(obj1); // { a: 10, b: 100, c: 30 } <-- b 被改到了 console.log(obj2); // { a: 10, b: 100, c: 30 }
復制一份obj1叫做obj2,然後把obj2.b改成100,但卻不小心改到obj1.b,因為他們根本是同一個對象,這就是所謂的淺拷貝。
要避免這樣的錯誤發生就要寫成這樣:
var obj1 = { a: 10, b: 20, c: 30 };var obj2 = { a: obj1.a, b: obj1.b, c: obj1.c }; obj2.b = 100; console.log(obj1); // { a: 10, b: 20, c: 30 } <-- b 沒被改到 console.log(obj2); // { a: 10, b: 100, c: 30 }
這樣就是深拷貝,不會改到原本的obj1
如何做到深拷貝
要完全復制又不能修改到原對象,這時候就要用深拷貝,下面是深拷貝的幾種方式:
1、自己手動復制
像上面的範例,把obj1的屬性一個個復制到obj2中:
var obj1 = { a: 10, b: 20, c: 30 }; var obj2 = { a: obj1.a, b: obj1.b, c: obj1.c }; obj2.b= 100; console.log(obj1); // { a: 10, b: 20, c: 30 } <-- 沒被改到 console.log(obj2); // { a: 10, b: 100, c: 30 }
但這樣很麻煩要自己慢慢復制,而且這樣其實不是深拷貝,如果像下面這個狀況。
var obj1 = { body: { a: 10 } }; var obj2 = { body: obj1.body }; obj2.body.a = 20; console.log(obj1); // { body: { a: 20 } } <-- 被改到了 console.log(obj2); // { body: { a: 20 } } console.log(obj1 === obj2); // false console.log(obj1.body === obj2.body); // true
雖然obj1跟obj2是不同對象,但他們會共享同一個obj1.body,所以修改obj2.body.a時也會修改到舊的。
2、Object.assign
Object.assign是 ES6 的新函數,可以幫助我們達成跟上面一樣的功能。
var obj1 = { a: 10, b: 20, c: 30 }; var obj2 = Object.assign({}, obj1); obj2.b = 100; console.log(obj1); // { a: 10, b: 20, c: 30 } <-- 沒被改到 console.log(obj2); // { a: 10, b: 100, c: 30 }
Object.assign({}, obj1)的意思是先建立一個空對象{},接著把obj1中所有的屬性復制過去,所以obj2會長得跟obj1一樣,這時候再修改obj2.b也不會影響obj1。因為Object.assign跟我們手動復制的效果相同,所以一樣只能處理深度只有一層的對象,沒辦法做到真正的深拷貝,不過如果要復制的對象只有一層的話可以考慮使用它。
如下有嵌套對象時,則沒有辦法做到深拷貝:
var obj1 = { a: 0 , b: { c: 0}}; var obj2 = Object.assign({}, obj1); console.log(obj2); // { a: 0, b: { c: 0}} obj1.a = 1; console.log(obj1); // { a: 1, b: { c: 0}} console.log(obj2); // { a: 0, b: { c: 0}} obj2.a = 2; console.log(obj1); // { a: 1, b: { c: 0}} console.log(obj2); // { a: 2, b: { c: 0}} obj2.b.c = 3; console.log(obj1); // { a: 1, b: { c: 3}} console.log(obj2); // { a: 2, b: { c: 3}}
3、轉成 JSON 再轉回來
用JSON.stringify把對象轉成字符串,再用JSON.parse把字符串轉成新的對象。
var obj1 = { body: { a: 10 } }; var obj2 = JSON.parse(JSON.stringify(obj1)); obj2.body.a = 20; console.log(obj1); // { body: { a: 10 } } <-- 沒被改到 console.log(obj2); // { body: { a: 20 } } console.log(obj1 === obj2); // false console.log(obj1.body === obj2.body); // false
這樣做是真正的Deep Copy,但只有可以轉成JSON格式的對象才可以這樣用,像function沒辦法轉成JSON。
var obj1 = { fun: function(){ console.log(123) } }; var obj2 = JSON.parse(JSON.stringify(obj1)); console.log(typeof obj1.fun); // ‘function‘ console.log(typeof obj2.fun); // ‘undefined‘ <-- 沒復制
要復制的function會直接消失,所以這個方法只能用在單純只有數據的對象。
4、jquery的$.extend
jquery 有提供一個$.extend可以用來做深拷貝:
var $ = require(‘jquery‘); var obj1 = { a: 1, b: { f: { g: 1 } }, c: [1, 2, 3] }; var obj2 = $.extend(true, {}, obj1); console.log(obj1.b.f === obj2.b.f); // false
5、lodash的_.cloneDeep
函數庫lodash提供_.cloneDeep用來做深拷貝:
var _ = require(‘lodash‘); var obj1 = { a: 1, b: { f: { g: 1 } }, c: [1, 2, 3] }; var obj2 = _.cloneDeep(obj1); console.log(obj1.b.f === obj2.b.f); // false
這是比較推薦的方法,性能還不錯,使用起來也很簡單。
深拷貝與淺拷貝