深拷貝是什麼?如何實現?
深拷貝(Deep Copy)是指在複製物件或陣列時,不僅複製其本身,還會遞迴地複製其內部的所有嵌套物件或陣列,創建一個完全獨立的副本。換句話說,原始物件和複製後的物件在記憶體中是完全獨立的,修改其中一個不會影響另一個。這種方式適用於包含嵌套結構(例如物件內含物件或陣列)的資料。
與之相對的是淺拷貝(Shallow Copy),淺拷貝只複製物件的第一層屬性,若屬性值是物件或陣列,則只複製其參考位址,導致原始物件和副本共享相同的嵌套物件,修改嵌套物件會同時影響兩者。
為什麼需要深拷貝?
當你需要操作一個物件的副本,但不希望影響原始物件時,就需要深拷貝。例如:
-
處理複雜的資料結構(如多層嵌套的物件或陣列)。
-
在前端應用中,確保狀態(state)或資料的獨立性,避免意外修改原始資料。
如何實現深拷貝?
在 JavaScript 中,實現深拷貝有幾種常見方法。以下我會詳細介紹,並提供完整的程式碼範例,方便你作為前端工程師可以輕鬆跟著操作。
方法 1:使用 JSON.parse(JSON.stringify())
這是最簡單的深拷貝方法,通過將物件轉為 JSON 字串再解析回物件,實現完全獨立的複製。
步驟:
-
使用
JSON.stringify()
將物件轉為 JSON 字串。 -
使用
JSON.parse()
將 JSON 字串轉回物件。
範例程式碼:
// 原始物件
const originalObj = {
name: '小明',
age: 25,
hobbies: ['閱讀', '跑步'],
address: {
city: '台北',
zip: '100'
}
};
// 執行深拷貝
const deepCopyObj = JSON.parse(JSON.stringify(originalObj));
// 修改副本
deepCopyObj.name = '小華';
deepCopyObj.hobbies.push('游泳');
deepCopyObj.address.city = '台中';
// 檢查原始物件和副本
console.log('原始物件:', originalObj);
console.log('副本物件:', deepCopyObj);
輸出結果:
原始物件: { name: '小明', age: 25, hobbies: ['閱讀', '跑步'], address: { city: '台北', zip: '100' } }
副本物件: { name: '小華', age: 25, hobbies: ['閱讀', '跑步', '游泳'], address: { city: '台中', zip: '100' } }
優點:
-
簡單易用,程式碼量少。
-
適用於大多數簡單的物件和陣列。
缺點:
-
不支援特殊資料類型(如 Date、RegExp、Function、循環參考等)。
-
如果物件中有 undefined 或 Symbol,會被忽略或轉為 null。
方法 2:使用遞迴自訂函數
如果需要處理更複雜的情況(例如包含 Date 或函數),可以撰寫一個自訂的深拷貝函數,遞迴地複製物件的每一層。
步驟:
-
檢查輸入是否為物件或陣列。
-
如果是物件或陣列,遍歷其屬性或元素,並遞迴複製。
-
處理特殊情況(如基本資料類型直接返回)。
範例程式碼:
function deepCopy(obj) {
// 如果不是物件或陣列,直接返回
if (obj === null || typeof obj !== 'object') {
return obj;
}
// 處理陣列
if (Array.isArray(obj)) {
const copy = [];
for (let i = 0; i < obj.length; i++) {
copy[i] = deepCopy(obj[i]); // 遞迴複製每個元素
}
return copy;
}
// 處理物件
const copy = {};
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
copy[key] = deepCopy(obj[key]); // 遞迴複製每個屬性
}
}
return copy;
}
// 測試範例
const originalObj = {
name: '小明',
age: 25,
hobbies: ['閱讀', '跑步'],
address: {
city: '台北',
zip: '100'
},
birth: new Date('2000-01-01')
};
// 執行深拷貝
const deepCopyObj = deepCopy(originalObj);
// 修改副本
deepCopyObj.name = '小華';
deepCopyObj.hobbies.push('游泳');
deepCopyObj.address.city = '台中';
deepCopyObj.birth.setFullYear(1999);
// 檢查原始物件和副本
console.log('原始物件:', originalObj);
console.log('副本物件:', deepCopyObj);
輸出結果:
原始物件: { name: '小明', age: 25, hobbies: ['閱讀', '跑步'], address: { city: '台北', zip: '100' }, birth: 2000-01-01T00:00:00.000Z }
副本物件: { name: '小華', age: 25, hobbies: ['閱讀', '跑步', '游泳'], address: { city: '台中', zip: '100' }, birth: 1999-01-01T00:00:00.000Z }
優點:
-
可以根據需求自訂,支援更多資料類型。
-
比 JSON.parse(JSON.stringify()) 更靈活。
缺點:
-
程式碼較複雜,需要自行處理特殊情況(如循環參考)。
-
如果物件結構非常深,遞迴可能導致效能問題。
方法 3:使用第三方庫(如 Lodash)
如果你的專案使用了 Lodash,可以直接使用它的 _.cloneDeep() 方法來實現深拷貝。
步驟:
-
安裝 Lodash(若尚未安裝):npm install lodash。
-
引入並使用 _.cloneDeep()。
範例程式碼:
// 引入 Lodash
const _ = require('lodash');
// 原始物件
const originalObj = {
name: '小明',
age: 25,
hobbies: ['閱讀', '跑步'],
address: {
city: '台北',
zip: '100'
}
};
// 執行深拷貝
const deepCopyObj = _.cloneDeep(originalObj);
// 修改副本
deepCopyObj.name = '小華';
deepCopyObj.hobbies.push('游泳');
deepCopyObj.address.city = '台中';
// 檢查原始物件和副本
console.log('原始物件:', originalObj);
console.log('副本物件:', deepCopyObj);
輸出結果:
原始物件: { name: '小明', age: 25, hobbies: ['閱讀', '跑步'], address: { city: '台北', zip: '100' } }
副本物件: { name: '小華', age: 25, hobbies: ['閱讀', '跑步', '游泳'], address: { city: '台中', zip: '100' } }
優點:
-
簡單且可靠,支援大多數特殊情況。
-
經過充分測試,適合生產環境。
缺點:
- 需要引入外部依賴,增加專案體積。
注意事項
-
循環參考:
-
如果物件有循環參考(例如物件 A 包含物件 B,物件 B 又參考物件 A),JSON.parse(JSON.stringify()) 會報錯,自訂函數也需要額外處理(例如使用 WeakMap 來追蹤已複製的物件)。
-
範例程式碼(處理循環參考):
function deepCopyWithCircular(obj, seen = new WeakMap()) {
if (obj === null || typeof obj !== 'object') {
return obj;
}
// 檢查是否已處理過此物件(避免循環參考)
if (seen.has(obj)) {
return seen.get(obj);
}
// 處理陣列
if (Array.isArray(obj)) {
const copy = [];
seen.set(obj, copy);
for (let i = 0; i < obj.length; i++) {
copy[i] = deepCopyWithCircular(obj[i], seen);
}
return copy;
}
// 處理物件
const copy = {};
seen.set(obj, copy);
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
copy[key] = deepCopyWithCircular(obj[key], seen);
}
}
return copy;
}
// 測試循環參考
const originalObj = {
name: '小明',
address: {
city: '台北'
}
};
originalObj.self = originalObj; // 製造循環參考
const deepCopyObj = deepCopyWithCircular(originalObj);
console.log('副本物件:', deepCopyObj);
-
-
特殊物件:
-
如果物件包含 Date、RegExp、函數等特殊類型,JSON.parse(JSON.stringify()) 無法正確處理,自訂函數需要額外邏輯。
-
例如,處理 Date:
function deepCopy(obj) {
if (obj === null || typeof obj !== 'object') {
return obj;
}
// 處理 Date
if (obj instanceof Date) {
return new Date(obj);
}
// 處理陣列
if (Array.isArray(obj)) {
const copy = [];
for (let i = 0; i < obj.length; i++) {
copy[i] = deepCopy(obj[i]);
}
return copy;
}
// 處理物件
const copy = {};
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
copy[key] = deepCopy(obj[key]);
}
}
return copy;
}
-
-
效能考量:
-
深拷貝對大物件或深層嵌套結構可能較耗時,需根據實際需求選擇方法。
-
簡單結構建議使用 JSON.parse(JSON.stringify()),複雜結構則考慮自訂函數或 Lodash。
-
總結
-
深拷貝確保物件完全獨立,適用於複雜資料結構。
-
實現方法:
-
JSON.parse(JSON.stringify()):簡單但有限制。
-
自訂遞迴函數:靈活但需自行處理特殊情況。
-
Lodash 的 _.cloneDeep():可靠但需引入依賴。
-