Skip to main content

深拷貝是什麼?如何實現?

深拷貝(Deep Copy)是指在複製物件或陣列時,不僅複製其本身,還會遞迴地複製其內部的所有嵌套物件或陣列,創建一個完全獨立的副本。換句話說,原始物件和複製後的物件在記憶體中是完全獨立的,修改其中一個不會影響另一個。這種方式適用於包含嵌套結構(例如物件內含物件或陣列)的資料。

與之相對的是淺拷貝(Shallow Copy),淺拷貝只複製物件的第一層屬性,若屬性值是物件或陣列,則只複製其參考位址,導致原始物件和副本共享相同的嵌套物件,修改嵌套物件會同時影響兩者。

為什麼需要深拷貝?

當你需要操作一個物件的副本,但不希望影響原始物件時,就需要深拷貝。例如:

  • 處理複雜的資料結構(如多層嵌套的物件或陣列)。

  • 在前端應用中,確保狀態(state)或資料的獨立性,避免意外修改原始資料。


如何實現深拷貝?

在 JavaScript 中,實現深拷貝有幾種常見方法。以下我會詳細介紹,並提供完整的程式碼範例,方便你作為前端工程師可以輕鬆跟著操作。

方法 1:使用 JSON.parse(JSON.stringify())

這是最簡單的深拷貝方法,通過將物件轉為 JSON 字串再解析回物件,實現完全獨立的複製。

步驟:

  1. 使用 JSON.stringify() 將物件轉為 JSON 字串。

  2. 使用 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 或函數),可以撰寫一個自訂的深拷貝函數,遞迴地複製物件的每一層。

步驟:

  1. 檢查輸入是否為物件或陣列。

  2. 如果是物件或陣列,遍歷其屬性或元素,並遞迴複製。

  3. 處理特殊情況(如基本資料類型直接返回)。

範例程式碼:

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() 方法來實現深拷貝。

步驟:

  1. 安裝 Lodash(若尚未安裝):npm install lodash。

  2. 引入並使用 _.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' } }

優點:

  • 簡單且可靠,支援大多數特殊情況。

  • 經過充分測試,適合生產環境。

缺點:

  • 需要引入外部依賴,增加專案體積。

注意事項

  1. 循環參考

    • 如果物件有循環參考(例如物件 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);
  2. 特殊物件

    • 如果物件包含 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;
      }
  3. 效能考量

    • 深拷貝對大物件或深層嵌套結構可能較耗時,需根據實際需求選擇方法。

    • 簡單結構建議使用 JSON.parse(JSON.stringify()),複雜結構則考慮自訂函數或 Lodash。


總結

  • 深拷貝確保物件完全獨立,適用於複雜資料結構。

  • 實現方法

    1. JSON.parse(JSON.stringify()):簡單但有限制。

    2. 自訂遞迴函數:靈活但需自行處理特殊情況。

    3. Lodash 的 _.cloneDeep():可靠但需引入依賴。