JavaScript 中的淺拷貝 (shallow copy) 和深拷貝 (deep copy) 差別是什麼?要如何實踐?

2024年1月3日

💎 加入 E+ 成長計畫 與超過 300+ 位軟體工程師一同在社群中成長,並且獲得更多的軟體工程學習資源

在 JavaScript 複製值時,當複製的是非原始型別 (primitive type) 的資料型別時,例如:物件(object)、陣列 (array) 等,會遇到淺拷貝 (shallow copy) 和深拷貝 (deep copy) 的差異。在面試時被問到這兩者的差異,你會怎麼回答? 如果要你當場手寫深拷貝,你會怎麼寫? 假如不確定的話,就一起來讀這篇吧。

比較淺拷貝 (shallow copy) 和深拷貝 (deep copy)

淺拷貝是指複製值時,滿足物件 A 與物件 B 不同,但物件 A 與 物件 B 有相同的屬性,並且屬性的原型鏈相同。

而深拷貝則是指在拷貝時,物件 A 與物件 B 不同,兩者在原型鏈上僅是結構相同,但其屬性實際的地址不同。在拷貝值時,有可能會遇到變數是多層的情境,例如是一個物件裡還有物件,深拷貝的定義會是每一層的值都不會共享址 (reference)。

這樣聽起來可能比較抽象,具體來說,以 lodash 這個套件提供的效用函式為例,有分成 clonecloneDeep 兩種不同效用函式,clone 只用於淺拷貝(第一層拷貝),但 cloneDeep 可用於深拷貝。下面的例子說明兩者的區別:

// lodash 的淺拷貝 clone
var objects = [{ a: 1 }, { b: 2 }];
var shallow = _.clone(objects);
console.log(objects === shallow); // false
console.log(shallow[0] === objects[0]); // true

// lodash 的深拷貝 cloneDeep
var objects = [{ a: 1 }, { b: 2 }];
var deep = _.cloneDeep(objects);
console.log(objects === deep); // false
console.log(deep[0] === objects[0]); // false

在說明完淺拷貝與深拷貝的差別後,面試中常見的接續問題是「手寫」淺拷貝與深拷貝。假如你不確定怎麼手寫這兩種拷貝方式,可以繼續往下看。

淺拷貝 (shallow copy)

方法一:手動複製值

let objA = {
  a: 1,
  b: { c: 3 },
};

let objB = { a: objA.a, b: objA.b };

console.log(objA === objB); // false
console.log(objA.b === objB.b); // true, 第二層的物件還是指向相同位置

方法二:使用 spread syntax

let objA = {
  a: 1,
  b: { c: 3 },
};

let objB = { ...objA };

console.log(objA === objB); // false
console.log(objA.b === objB.b); // true, 第二層的物件還是指向相同位置

方法三:使用 Object.assign

let objA = {
  a: 1,
  b: { c: 3 },
};

let objB = Object.assign({}, objA);

console.log(objA === objB); // false
console.log(objA.b === objB.b); // true, 第二層的物件還是指向相同位置

深拷貝 (deep copy)

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

這個作法是先將物件用 JSON.stringify 序列化為 string,再透過 JSON.parse 轉換回物件。要特別注意,這做法只能用於可序列化的物件,有些無法序列化的物件例如:function、HTML 的元素,這些是無法序列化的,所以執行前,需要先確認是否可以序列化,否則在執行 JSON.stringify 時會失敗。

let objA = {
  a: 1,
  b: { c: 3 },
};

function deepCopy(item) {
  return JSON.parse(JSON.stringify(item));
}

let objB = deepCopy(objA);

console.log(objA === objB); // false
console.log(objA.b === objB.b); // false

方法二:使用 structuredClone(value)

針對可序列化的物件,有另外一種透過 JavaScript 內建的方法達成深拷貝。這種方法是 structuredClone(value),用法如下。

let objA = {
  a: 1,
  b: { c: 3 },
};

let objB = structuredClone(objA);

console.log(objA === objB); // false
console.log(objA.b === objB.b); // false

方法三:考慮多重情況的遞迴式深拷貝

通常在面試中,用上述兩種方式,可能會被面試官追問說,如果不用這種現成的方法,要如何手寫。以下的寫法是透過遞迴的方式,來進行深拷貝。

function deepClone(obj, cache = new WeakMap()) {
  if (cache.has(obj)) {
    return cache.get(obj);
  }

  if (obj === null || typeof obj !== "object" || typeof value === "function") {
    return obj;
  }

  if (obj instanceof Date) return new Date(obj);
  if (obj instanceof RegExp) return new RegExp(obj);

  const result = Array.isArray(obj)
    ? []
    : Object.create(Object.getPrototypeOf(obj));

  cache.set(obj, result);

  for (const key of Reflect.ownKeys(obj)) {
    const value = obj[key];
    result[key] = deepClone(value, cache);
  }

  return result;
}
🧵 如果你想收到最即時的內容更新,可以在 FacebookInstagram 上追蹤我們