當初一開始在學 JS 時就常常看到淺拷貝 & 深拷貝這兩個詞,只是可惜一直沒有做個整理,所以這篇文章要來整理一下相關的觀念。
首先我們知道 JS 變數類型分為兩大類:
原始型別都具有 passing by value 傳值特性,在複製一個變數時,是直接複製被複製變數的值。
比如當宣告一個變數 a 並為它賦值 5,若宣告變數 b 並複製變數 a,就算修改 b 的值也不會修改到 a 的值。
let a = 1;
let b = a;
b = 2;
console.log(a, b); // 1, 2
物件型別則具有 passing by reference 傳值特性,在複製一個變數時,是複製被複製變數的記憶體位置,而相同的記憶體位置裡面又存著變數的值。
所以當宣告一個變數 c 並為一個空物件時,若宣告變數 d 並複製變數 c,因為兩個變數參考到的記憶體位置相同,c 增加了屬性 name,d 也會跟著增加相同的屬性和屬性值。
const c = {};
const d = c;
c.name = 'Harry';
console.log(c, d); // { name: 'Harry' } { name: 'Harry' }
而上面這種單純複製記憶體位置的方式就稱為淺拷貝,而相對的,若複製一份全新的記憶體位置和值就稱為深拷貝。
此外,我們知道物件或是陣列有時會有巢狀的情況,例如像物件內還有另一個物件當作屬性,或是二維、陣列內多個物件當元素等,而這些巢狀的物件若還是有參考到相同的記憶體位置,那就只能算是淺拷貝。
以下介紹一些淺拷貝 & 深拷貝方式:
const member = ['Jack', 'Mike', 'Tom', 'Marry'];
// 方法1,透過 Array.from 建立的新陣列
const memberArrayFrom = Array.from(member);
// 方法2,展開運算符
const memberSpreadParams = [...member];
// 方法3,slice()
const memberSlice = member.slice();
// 方法4,concat()
const memberConcat = [].concat(member);
const myDog = {
name: 'lucky',
age: 5
};
// 方法1,Object.assign 創造新物件
const myDogObjAssign = Object.assign({}, myDog, { name: 'puppy', age: 2 });
// 方法2,展開運算符
const myDogSpreadParams = { ...myDog, name: 'puppy', age: 2 };
// 方法3(深拷貝),JSON.parse & JSON.stringify
const myDogDeepCopy = JSON.parse(JSON.stringify(myDog));
最後一種方式 JSON.parse + JSON.stringify 可以做深拷貝,只是會有些小問題,例如以下範例中將深拷貝後的物件印出來後,可以從截圖看到一些值被改變了。
const specialCase = {
undefinedProperty: undefined,
notANumber: NaN,
infinityValue: Infinity,
regExp: /^A/,
date: new Date(2022, 9, 1),
map: new Map(),
set: new Set(),
};
const cloneObj = JSON.parse(JSON.stringify(specialCase));
若要使用深拷貝可以用第三方函式庫 Lodash 提供的 _.cloneDeep 函式:
import _ from "lodash";
const objects = [{ 'a': 1 }, { 'b': 2 }];
const deep = _.cloneDeep(objects);
console.log(deep[0] === objects[0]);
// => false
Ramda 也有類似的函式喔~
也可以透過 JS 內建的 structuredClone() 去做深拷貝:
// Create an object with a value and a circular reference to itself.
const original = { name: "MDN" };
original.itself = original;
// Clone it
const clone = structuredClone(original);
console.assert(clone !== original); // the objects are not the same (not same identity)
console.assert(clone.name === "MDN"); // they do have the same values
console.assert(clone.itself === clone); // and the circular reference is preserved
自己也能實作一個簡單版本的,如果要做的很完整當然還有些地方可優化:
const deepCopy = (inputObj) => {
// 不是物件就直接回傳
if (typeof inputObj !== 'object' || inputObj === null) return inputObj;
// 處理特殊物件型態
if (inputObj instanceof Date || inputObj instanceof RegExp) {
return inputObj.constructor(inputObj);
}
const result = Array.isArray(inputObj) ? [] : {};
for(let key in inputObj) {
// 如果屬性也是物件,就進行遞迴
result[key] = deepCopy(inputObj[key]);
}
return result;
}
const nestedArr = [[1], [2], [3]];
const copyNestedArr = deepCopy(nestedArr);
const nestedObj = {
memberNum: 10,
level: {
glod: 2,
silver: 3,
bronze: 5,
},
};
const copyNestedObj = deepCopy(nestedObj);
JS 中的淺拷貝 (Shallow copy) 與深拷貝 (Deep copy) 原理與實作