JavaScript 深拷貝(deep copy)和淺拷貝(shallow copy)


參考:

  1.  【進階4-1期】詳細解析賦值、淺拷貝和深拷貝的區別
  2. How to differentiate between deep and shallow copies in JavaScript

 

在編程語言中,數據或者值是存放在變量中的。拷貝的意思就是使用相同的值創建新的變量。

當我們改變拷貝的東西時,我們不希望原來的東西也發生改變。

 

深拷貝的意思是這個新變量里的值都是從原來的變量中復制而來,並且和原來的變量沒有關聯。

淺拷貝的意思是,新變量中存在一些仍然與原來的變量有關聯的值。

 

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 原生的數據類型(非自定義數據類型)。

而且存在問題

  1. 會忽略 undefined
  2. 會忽略 symbol
  3. 不能序列化函數
  4. 不能解決循環引用的對象
// 木易楊
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()

 總結:


免責聲明!

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



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