參考:
在編程語言中,數據或者值是存放在變量中的。拷貝的意思就是使用相同的值創建新的變量。
當我們改變拷貝的東西時,我們不希望原來的東西也發生改變。
深拷貝的意思是這個新變量里的值都是從原來的變量中復制而來,並且和原來的變量沒有關聯。
淺拷貝的意思是,新變量中存在一些仍然與原來的變量有關聯的值。
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()