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 物件
可以處理更複雜的資料結構
使用步驟:
安裝 Lodash:
npm install lodash
npm install @types/lodash // 如果使用 TypeScript
在檔案中引入:
import _ from 'lodash';
這樣,當用戶取消編輯時,原始數據不會被影響:
onCancelEdit() {
this.isEditing = false;
this.selectedHero = this.heroes.find((hero) => hero.selected);
}
實踐
顯示時使用參考:當只需要顯示數據時,直接使用物件參考即可,不需要創建複本。
編輯時使用深拷貝:當需要編輯數據,且希望在取消時能夠還原時,使用深拷貝創建獨立的複本。
// 推薦的做法
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));
}
}
注意事項
選擇適當的深拷貝方法:
JSON.parse(JSON.stringify())
的限制:無法處理循環參考
會丟失函數和 undefined 值
無法正確處理 Date 物件
Lodash
cloneDeep
的優勢:可以處理所有上述情況
效能通常更好
更可靠的深拷貝實現
效能考量:
深拷貝會消耗更多記憶體和處理時間
只在必要時使用深拷貝
對於大型物件,考慮使用專門的深拷貝函式庫(如 lodash 的 cloneDeep)
結論
在處理物件資料時,理解參考、淺拷貝和深拷貝的概念非常重要。選擇適當的拷貝方式取決於具體需求:
需要共享數據時使用參考
需要獨立副本時使用深拷貝
需要在效能和功能之間取得平衡時,根據實際情況選擇合適的方案
Last updated