JavaScript 深拷貝(deep copy)和淺拷貝(shallow copy)
參考:
在程式語言中,資料或者值是存放在變數中的。拷貝的意思就是使用相同的值建立新的變數。
當我們改變拷貝的東西時,我們不希望原來的東西也發生改變。
深拷貝 的意思是這個新變數裡的值都是從原來的變數中複製而來,並且和原來的變數沒有關聯。
淺拷貝 的意思是,新變數中存在一些仍然與原來的變數有關聯的值。
JavaScript 資料型別
原始資料型別 (有的資料叫做基本資料型別):數字、字串、布林值、undefined、null
這些值被賦值後就和對應的變數繫結在一起。如果你拷貝這些變數,就是實實在在的拷貝。
b = a 就是一次拷貝,重新給 b 賦值,a 的值不會改變:
const a = 5 let b = a // this is the copy b = 6 console.log(b) // 6 console.log(a) // 5
複合資料型別 (有的資料叫做引用資料型別)——物件 和 陣列
技術上講,陣列也是物件。
這種型別的值,只在初始化的時候儲存一次。賦值給變數也僅僅是建立了一個指向這個值的引用。
拷貝 b = a,改變 b 中的屬性 pt 的值,a 中包含的 pt 的值也改變了,因為 a 和 b 實際上指向的是同一個物件:
const a = { en: 'Hello', de: 'Hallo', es: 'Hola', pt: 'Olà' } let b = a b.pt = 'Oi' console.log(b.pt) // Oi console.log(a.pt) // Oi
上面這個例子就是一個 淺拷貝 。
新的物件有著原物件屬性的一份精確拷貝。如果屬性值是原始型別,拷貝的就是原始型別值,如果屬性是引用型別,拷貝的就是記憶體地址,如果其中一個物件改變了這個地址或者改變了記憶體中的值,另一個物件的屬性也會變化。
也就是說淺拷貝只拷貝了第一層的原始型別值,和第一層的引用型別地址。
淺拷貝的場景 :
展開操作符 Spread operator
使用這個操作符可以將所有的屬性值複製到新物件中。
const a = { en: 'Bye', de: 'Tschüss' } let b = {...a} b.de = 'Ciao' console.log(b.de) // Ciao console.log(a.de) // Tschüss
還可以合併兩個物件,比如 const c = { ...a, ...b}.
Object.assign()
用於將所有可列舉屬性的值從一個或多個源物件複製到目標物件,然後返回目標物件。
第一個引數是被修改和最終返回的值,第二個引數是你要拷貝的物件。通常,只需要給第一個引數傳入一個空物件,這樣可以避免修改已有的資料。
const a = { en: 'Bye', de: 'Tschüss' } let b = Object.assign({}, a) b.de = 'Ciao' console.log(b.de) // Ciao console.log(a.de) // Tschüss
拷貝陣列
const a = [1,2,3] let b = [...a] b[1] = 4 console.log(b[1]) // 4 console.log(a[1]) // 2
陣列方法——map, filter, reduce
這些方法都可以返回新的陣列:
const a = [1,2,3] let b = a.map(el => el) b[1] = 4 console.log(b[1]) // 4 console.log(a[1]) // 2
在拷貝的過程中修改特定的值:
const a = [1,2,3] const b = a.map((el, index) => index === 1 ? 4 : el) console.log(b[1]) // 4 console.log(a[1]) // 2
Array.slice
使用array.slice() 或者 array.slice(0) 你可以得到原陣列的拷貝。
const a = [1,2,3] let b = a.slice(0) b[1] = 4 console.log(b[1]) // 4 console.log(a[1]) // 2
巢狀物件或陣列
就算使用了上面的方法,如果物件內部包含物件,那麼內部巢狀的物件也不會被拷貝,因為它們只是引用。因此改變巢狀物件,所有的例項中的巢狀物件的屬性都會被改變。 所以說上面的場景全部都只實現了淺拷貝 。
const a = { foods: { dinner: 'Pasta' } } let b = {...a} b.foods.dinner = 'Soup' // changes for both objects console.log(b.foods.dinner) // Soup console.log(a.foods.dinner) // Soup
深拷貝
拷貝所有屬性,並拷貝屬性指向的動態分配的記憶體。深拷貝時物件和所引用的物件一起拷貝,相比淺拷貝速度較慢且花銷大。拷貝物件和原物件互不影響。
對巢狀的物件進行深拷貝,一種方法是手動拷貝所有巢狀的物件。
const a = { foods: { dinner: 'Pasta' } } let b = {foods: {...a.foods}} b.foods.dinner = 'Soup' console.log(b.foods.dinner) // Soup console.log(a.foods.dinner) // Pasta
如果物件除了 foods 之外還有很多屬性,仍然可以利用展開操作符,比如
const b = {...a, foods: {...a.foods}}.
如果你不知道這個巢狀結構的深度,那麼手動遍歷這個物件然後拷貝每個巢狀的物件就很麻煩了。
一個很簡單的方法就是使用 JSON.stringify 和 JSON.parse
const a = { foods: { dinner: 'Pasta' } } let b = JSON.parse(JSON.stringify(a)) b.foods.dinner = 'Soup' console.log(b.foods.dinner) // Soup console.log(a.foods.dinner) // Pasta
但是這裡要注意的是,你只能使用這種方法拷貝 JavaScript 原生的資料型別( 非自定義資料型別 )。
而且存在 問題 :
- 會忽略 undefined
- 會忽略 symbol
- 不能序列化函式
- 不能解決迴圈引用的物件
// 木易楊 let obj = { name: 'muyiy', a: undefined, b: Symbol('muyiy'), c: function() {} } console.log(obj); // { //name: "muyiy", //a: undefined, //b: Symbol(muyiy), //c: ƒ () // } let b = JSON.parse(JSON.stringify(obj)); console.log(b); // {name: "muyiy"}
// 木易楊 let obj = { a: 1, b: { c: 2, d: 3 } } obj.a = obj.b; obj.b.c = obj.a; let b = JSON.parse(JSON.stringify(obj)); // Uncaught TypeError: Converting circular structure to JSON
拷貝自定義型別的例項
你不能使用 JSON.stringify 和 JSON.parse 來拷貝自定義型別的資料,下面的例子使用一個自定義的 copy() 方法:
class Counter { constructor() { this.count = 5 } copy() { const copy = new Counter() copy.count = this.count return copy } } const originalCounter = new Counter() const copiedCounter = originalCounter.copy() console.log(originalCounter.count) // 5 console.log(copiedCounter.count) // 5 copiedCounter.count = 7 console.log(originalCounter.count) // 5 console.log(copiedCounter.count) // 7
如果例項中有其它物件的引用,就要在copy方法中使用 JSON.stringify 和 JSON.parse 。
除此之外,深拷貝方法還有 jQuery.extend() 和 lodash.cloneDeep()
總結: