Skip to main content

Shallow Copy & Deep Copy

型別

JavaScript內建的型別主要可以分成基本型別(Primitives)與物件型別 (Object)兩大類。

而基本型別又分成 stringnumberbooleannullundefinedsymbol幾種,除了以上幾種之外,其他都可以歸類至物件型別。

這二種型別之間的差異,就是在他們的傳值方式:

基本型別 => 傳「值」(value)
物件型別 => 傳「址」(reference)

基本型別

let a = "apple";
let b = a;
b = "banana";
console.log(a); // apple
console.log(b); // banana

在修改 b 時並不會改到 a 的值。

物件型別

let objA = { name: 'apple' }
let objB = objA
objB.name = 'banana'
console.log(objA); // { name: 'banana' }
console.log(objB); // { name: 'banana' }

在修改 objB 時,也會修改到 objA 的值。

Shallow Copy(淺拷貝)與 Deep Copy(深拷貝)

  • 淺拷貝 — 只能完成第一層的淺層複製,若有第二層結構時,還是依據參考特性作處理,也就代表指向記憶體位址還是一樣的。

  • 深拷貝 — 深度複製指定物件,操作新物件不影響原物件,兩者指向不同記憶體位址。

淺拷貝方法 — Object.assign

Object.assign 是 ES6 的新函式,我們可以用來達成複製的功能。

let array = ['red', 'blue', 'yellow'];
let object = { p1: '1', p2: '2', p3: '3' };

let arrayCP = Object.assign([], array);
arrayCP[0] = 'black';

let objectCP = Object.assign({}, object);
objectCP.p2 = '4';

console.log(array); // ['red', 'blue', 'yellow'] <= 原陣列沒有影響
console.log(arrayCP); // ['black', 'blue', 'yellow']
console.log(object); // { p1: '1', p2: '2', p3: '3' } <= 原物件沒有影響
console.log(objectCP); // { p1: '1', p2: '4', p3: '3' }

雖然目前看起來都沒受影響,但第二層還是會有參考特性影響原物件問題:

let data = [{ name: 'Yukai', age: 25 }];
let dataCP = Object.assign([], data);
// 操作第一層 : 不影響原物件
dataCP.push({ name: 'Amy', age: 50 });
// 操作第二層 : 影響原物件
dataCP[0].name = 'Tom';

console.log(data); // [{ name: 'Tom', age: 25 }]
console.log(dataCP); // [{ name: 'Tom', age: 25 }, { name:'Amy', age: 50 }]

淺拷貝方法 — 展開運算符(Spread Operator)

展開運算符也是 ES6 新增的特性,主要功能是把一個陣列展開(expand)成個別值,在依序放入指定物件或陣列,也可用做淺層複製:

let array = ['red', 'blue', 'yellow'];
let object = { p1: '1', p2: '2', p3: '3' };

let arrayCP = [...array];
arrayCP[0] = 'black';

let objectCP = { ...object };
objectCP.p2 = '4';

console.log(array); // ['red', 'blue', 'yellow'] <= 原陣列沒有影響
console.log(arrayCP); // ['black', 'blue', 'yellow']
console.log(object); // { p1: '1', p2: '2', p3: '3' } <= 原物件沒有影響
console.log(objectCP); // { p1: '1', p2: '4', p3: '3' }

但如同 Object.assign,第二層也會有參考特性影響原物件問題。

深拷貝方法 — JSON

常見應用為 Local Storge 等存儲操作,但也可以應用在深拷貝,主要利用 JSON.stringify 把物件轉成字串,再用 JSON.parse 把字串轉為物件:

let data = [{ name: 'Yukai', age: 25 }];
let dataCP = JSON.parse(JSON.stringify(data));
// 操作第一層:不影響原物件
dataCP.push({ name: 'Amy',age:50 });
// 操作第二層:不影響原物件
dataCP[0].name = 'Tom';
console.log(data); // [{ name: 'Yukai', age: 25 }]
console.log(dataCP); // [{ name: 'Tom', age: 25 }, { name: 'Amy', age: 50 }]
限制與缺點
  • 無法處理循環引用:如果對象中存在循環引用(例如,對象的一個屬性直接或間接地引用了對象本身),這種方法會拋出錯誤。
  • 忽略屬性值:在拷貝過程中,對象中的任意的函式、undefined 以及 symbol 值會被忽略。
  • 特殊對象處理不足:不能處理像 Date、RegExp、Function、Map、Set 等特殊類型的對象。

深拷貝方法 — structuredClone

它是一個全局方法,用於創建給定值的深拷貝,使用的是結構化克隆算法,這意味著它不僅複製了對象本身,還複製了所有嵌套的對象:

const original = {
name: "Alice",
age: 25,
contact: {
email: "alice@example.com",
phone: "1234567890"
}
};
const clone = structuredClone(original);
clone.contact.email = "alice_clone@example.com";
console.log(original); // { name: 'Alice', age: 25, contact: { email: 'alice@example.com', phone: '1234567890' } }
console.log(clone); // { name: 'Alice', age: 25, contact: { email: 'alice_clone@example.com', phone: '1234567890' } }
限制與缺點
  • 不支援所有類型:雖然 structuredClone 支援許多 JavaScript 內建類型,但它不支援某些專門的對象類型,如函式、錯誤對象、DOM 節點等。嘗試克隆這些不支援的類型會導致錯誤。
  • 可能改變原始對象:如果原始對象包含可轉移對象(如 ArrayBuffer),在克隆過程中這些對象會從原始對象轉移到新對象,使得它們在原始對象中不再可用。

深拷貝方法 — 遞迴實現

通過遞迴方式複製每個屬性(包括嵌套的對象和數組)來實現深拷貝。這保證了拷貝對象在結構上與原始對象完全獨立,對拷貝對象的任何修改都不會影響原始對象,反之亦然:

function deepCopy(obj) {
// 如果 obj 是基本類型之一,將直接返回
if (obj === null || typeof obj !== 'object') {
return obj;
}

// 判斷 obj 是物件還是陣列,返回空物件或空陣列
let tempObj = Array.isArray(obj) ? [] : {};

// 每個屬性的拷貝被賦值給 tempObj 的對應屬性
for (let key in obj) {
tempObj[key] = deepCopy(obj[key]);
}

// 返回深拷貝的對象
return tempObj;
}

const originalObj = {
name: 'Alice',
age: 30,
address: {
street: '123 Main St',
city: 'Wonderland'
},
hobbies: ['reading', 'coding']
};

const copiedObj = deepCopy(originalObj);

// 修改原始對象
originalObj.address.city = 'New Wonderland';
originalObj.hobbies.push('painting');

// 修改拷貝对象
copiedObj.name = 'Bob';

console.log(originalObj); // address和hobbies被修改,name保持不變
console.log(copiedObj); // name被修改,address和hobbies保持不變
限制與缺點
  • 特殊對象處理不足:不能處理像 Date、RegExp、Function、Map、Set 等特殊類型的對象。