JavaScript 深拷貝與淺拷貝

問題情境

在我的 Angular Tour of Heroes 專案中,首次遇到了深拷貝與淺拷貝的問題 🎉

當使用者取消編輯英雄資料時,即使我重新從原始陣列中獲取數據,賦值給被選中的英雄資料(因為 ngModel 是綁被選中的英雄資料,所以在取消編輯的時候要把原始資料覆蓋回去),修改過的內容仍然存在。

深拷貝淺拷貝這篇講得很清楚,推推。這邊只寫我專案的情況。

關於JS中的淺拷貝(shallow copy)以及深拷貝(deep copy) | by Andy Chen | Andy的技術分享blog | Medium

物件參考

在 JavaScript 中,當我們將一個物件賦值給變數時,實際上是創建了一個指向該物件的參考,而不是創建一個新的複本。也就是說他其實只是將兩個東西都指向了同一個記憶體位置。

問題示例

// 原始程式碼
onSelectHero(id: number) {
  this.heroes = this.heroes.map((hero) => ({
    ...hero,
    selected: hero.id === id,
  }));

  // 這裡只是創建了一個參考,而不是複本
  this.selectedHero = this.heroes.find((hero) => hero.id === id);
}

當我們通過 ngModel 修改 selectedHero 的屬性時:

<input [(ngModel)]="selectedHero!.name">

實際上是在直接修改原始陣列中的物件,因為 selectedHero 和陣列中的物件指向同一個記憶體位置。

淺拷貝 (Shallow Copy)

淺拷貝會創建一個新物件,但只複製第一層屬性的值。如果屬性是物件或陣列,則只複製它們的參考。

使用展開運算符進行淺拷貝

const hero = { id: 1, name: 'Iron Man', type: ['Technology'] };
const copy = { ...hero };

// 修改 copy.name 不會影響原物件
copy.name = 'Tony Stark';
console.log(hero.name);  // 仍然是 'Iron Man'

// 但修改 copy.type 會影響原物件,因為是同一個陣列的參考
copy.type.push('Flight');
console.log(hero.type);  // ['Technology', 'Flight']

深拷貝 (Deep Copy)

深拷貝會創建一個完全獨立的新物件,包括所有巢狀物件和陣列。實現深拷貝有幾種常見方法:

1. 使用 JSON 方法進行深拷貝

// 解決方案:在開始編輯時創建深拷貝
onStartEdit() {
  this.isEditing = true;
  if (this.selectedHero) {
    this.selectedHero = JSON.parse(JSON.stringify(this.selectedHero));
  }
}

2. 使用 Lodash 的 cloneDeep 方法

Lodash 提供了一個強大的深拷貝方法 cloneDeep,相比 JSON 方法有更多優勢:

import _ from 'lodash';

onStartEdit() {
  this.isEditing = true;
  if (this.selectedHero) {
    this.selectedHero = _.cloneDeep(this.selectedHero);
  }
}

Lodash cloneDeep 的優點:

  • 可以處理循環參考

  • 保留函數和 undefined 值

  • 正確處理 Date 物件

  • 可以處理更複雜的資料結構

使用步驟:

  1. 安裝 Lodash:

npm install lodash
npm install @types/lodash  // 如果使用 TypeScript
  1. 在檔案中引入:

import _ from 'lodash';

這樣,當用戶取消編輯時,原始數據不會被影響:

onCancelEdit() {
  this.isEditing = false;
  this.selectedHero = this.heroes.find((hero) => hero.selected);
}

實踐

  1. 顯示時使用參考:當只需要顯示數據時,直接使用物件參考即可,不需要創建複本。

  2. 編輯時使用深拷貝:當需要編輯數據,且希望在取消時能夠還原時,使用深拷貝創建獨立的複本。

// 推薦的做法
onSelectHero(id: number) {
  // 更新 selected 狀態
  this.heroes = this.heroes.map((hero) => ({
    ...hero,
    selected: hero.id === id,
  }));

  // 一般顯示時直接使用參考
  this.selectedHero = this.heroes.find((hero) => hero.id === id);
}

onStartEdit() {
  this.isEditing = true;
  // 開始編輯時才創建深拷貝
  if (this.selectedHero) {
    this.selectedHero = JSON.parse(JSON.stringify(this.selectedHero));
  }
}

注意事項

  1. 選擇適當的深拷貝方法

    • JSON.parse(JSON.stringify()) 的限制:

      • 無法處理循環參考

      • 會丟失函數和 undefined 值

      • 無法正確處理 Date 物件

    • Lodash cloneDeep 的優勢:

      • 可以處理所有上述情況

      • 效能通常更好

      • 更可靠的深拷貝實現

  2. 效能考量

    • 深拷貝會消耗更多記憶體和處理時間

    • 只在必要時使用深拷貝

    • 對於大型物件,考慮使用專門的深拷貝函式庫(如 lodash 的 cloneDeep)

結論

在處理物件資料時,理解參考、淺拷貝和深拷貝的概念非常重要。選擇適當的拷貝方式取決於具體需求:

  • 需要共享數據時使用參考

  • 需要獨立副本時使用深拷貝

  • 需要在效能和功能之間取得平衡時,根據實際情況選擇合適的方案

Last updated