淺拷貝 (Shallow Copy) 與深拷貝 (Deep Copy)
Node.js 淺拷貝 (Shallow Copy) 與深拷貝 (Deep Copy)
資料傳遞方式
call by value 傳值
- String
- Number
- Boolean
- Null
- Undefined
- Symbol
當複製變數時,會將變數的 數值
複製到新的變數,複製的變數被修改,不會
影響到原始變數
let Employee = 'Kay';
let directCopyEmployee = Employee;
directCopyEmployee = 'Jay';
// Kay
console.log(Employee);
// Jay
console.log(directCopyEmployee);
call by reference 傳址
- Object
- Array
- Function
當複製變數時,會將變數的 reference 位址
指定到新的變數,複製的變數被修改,會
影響到原始變數
let Employee = {
name: 'Kay',
age: 17
};
let directCopyEmployee = Employee;
directCopyEmployee.name = 'Jay';
// { name: 'Jay', age: 17 }
console.log(Employee);
// { name: 'Jay', age: 17 }
console.log(directCopyEmployee);
什麼是淺拷貝 (Shallow Copy) 與深拷貝 (Deep Copy)?
淺拷貝 (Shallow Copy)
- 只能完成
第一層
的淺層複製,若有第二層結構時,還是依據referene 參考特性
處理,也就代表指向記憶體位址還是一樣的。
深拷貝 (Deep Copy)
- 完整深度複製指定物件,包含所有層及的資料
- 操作新物件不影響原物件,兩者指向不同記憶體位址。
淺拷貝 (Shallow Copy) 方法
- 物件有 2 層以上結構,使用淺拷貝,改變第 2 層資料,
會影響
原有物件
Array 陣列
使用 Object.assign() 複製
let Employee = ['Kay', 'Jay', () => {}];
let directCopyEmployee = Employee;
let shallowCopyEmployee = Object.assign([], Employee);
directCopyEmployee[1] = 'KJ';
shallowCopyEmployee[1] = 'Shallow';
// [ 'Kay', 'KJ', [Function (anonymous)] ]
console.log(Employee);
// [ 'Kay', 'KJ', [Function (anonymous)] ]
console.log(directCopyEmployee);
// [ 'Kay', 'Shallow', [Function (anonymous)] ]
console.log(shallowCopyEmployee);
// true: reference 原始物件
console.log(Employee === directCopyEmployee);
// false: 建立全新物件
console.log(Employee === shallowCopyEmployee);
物件有 2 層以上結構,使用淺拷貝,改變第 2 層資料,會影響
原有物件
let Employee = [
'Kay',
'Jay',
['Apple', 'Pen']
];
let directCopyEmployee = Employee;
let shallowCopyEmployee = Object.assign([], Employee);
directCopyEmployee[1] = 'KJ';
// 改變第 1 層資料,不影響原有物件
shallowCopyEmployee[1] = 'Shallow';
// 改變第 2 層資料,會影響原有物件
shallowCopyEmployee[2][0] = 'Pineapple';
// [ 'Kay', 'KJ', [ 'Pineapple', 'Pen' ] ]
console.log(Employee);
// [ 'Kay', 'KJ', [ 'Pineapple', 'Pen' ] ]
console.log(directCopyEmployee);
// [ 'Kay', 'Shallow', [ 'Pineapple', 'Pen' ] ]
console.log(shallowCopyEmployee);
// true: reference 原始物件
console.log(Employee === directCopyEmployee);
// false: 建立全新物件
console.log(Employee === shallowCopyEmployee);
// false: 第一層數值資料完整複製
console.log(Employee[1] === shallowCopyEmployee[1]);
// true: 第二層 reference 原始物件第 2 層
console.log(Employee[2] === shallowCopyEmployee[2]);
使用 Array.concat() 複製
let Employee = ['Kay', 'Jay', () => {}];
let shallowCopyEmployee = Employee.concat();
shallowCopyEmployee[1] = 'Shallow';
// [ 'Kay', 'Jay', [Function (anonymous)] ]
console.log(Employee);
// [ 'Kay', 'Shallow', [Function (anonymous)] ]
console.log(shallowCopyEmployee);
使用 Array.slice() 複製
let Employee = ['Kay', 'Jay', () => {}];
let shallowCopyEmployee = Employee.slice();
shallowCopyEmployee[1] = 'Shallow';
// [ 'Kay', 'Jay', [Function (anonymous)] ]
console.log(Employee);
// [ 'Kay', 'Shallow', [Function (anonymous)] ]
console.log(shallowCopyEmployee);
使用 Array.map() 複製
let Employee = ['Kay', 'Jay', () => {}];
let shallowCopyEmployee = Employee.map(x => x);
shallowCopyEmployee[1] = 'Shallow';
// [ 'Kay', 'Jay', [Function (anonymous)] ]
console.log(Employee);
// [ 'Kay', 'Shallow', [Function (anonymous)] ]
console.log(shallowCopyEmployee);
使用 Array.filter() 複製
let Employee = ['Kay', 'Jay', () => {}];
let shallowCopyEmployee = Employee.filter(() => true);
shallowCopyEmployee[1] = 'Shallow';
// [ 'Kay', 'Jay', [Function (anonymous)] ]
console.log(Employee);
// [ 'Kay', 'Shallow', [Function (anonymous)] ]
console.log(shallowCopyEmployee);
使用 Array.reduce() 複製
let Employee = ['Kay', 'Jay', () => {}];
let shallowCopyEmployee = Employee.reduce((newArray, element) => {
newArray.push(element);
return newArray;
}, []);
shallowCopyEmployee[1] = 'Shallow';
// [ 'Kay', 'Jay', [Function (anonymous)] ]
console.log(Employee);
// [ 'Kay', 'Shallow', [Function (anonymous)] ]
console.log(shallowCopyEmployee);
使用 Array.from() 複製
let Employee = ['Kay', 'Jay', () => {}];
let shallowCopyEmployee = Array.from(Employee);
shallowCopyEmployee[1] = 'Shallow';
// [ 'Kay', 'Jay', [Function (anonymous)] ]
console.log(Employee);
// [ 'Kay', 'Shallow', [Function (anonymous)] ]
console.log(shallowCopyEmployee);
使用 […Array] 展開運算元複製
let Employee = ['Kay', 'Jay', () => {}];
let shallowCopyEmployee = [...Employee];
shallowCopyEmployee[1] = 'Shallow';
// [ 'Kay', 'Jay', [Function (anonymous)] ]
console.log(Employee);
// [ 'Kay', 'Shallow', [Function (anonymous)] ]
console.log(shallowCopyEmployee);
使用 JSON.stringify() 及 JSON.parse() 複製
注意!
函數
不可以被拷貝
let Employee = ['Kay', 'Jay', () => {}];
let shallowCopyEmployee = JSON.parse( JSON.stringify(Employee));
shallowCopyEmployee[1] = 'Shallow';
// [ 'Kay', 'Jay', [Function (anonymous)] ]
console.log(Employee);
// [ 'Kay', 'Shallow', null ]
console.log(shallowCopyEmployee);
自製淺拷貝函式
let customShallowCopy = function (OriginalObject) {
// 只拷貝物件
if (typeof OriginalObject !== 'object') {
return OriginalObject;
}
// 根據 OriginalObject 的類型,判斷要新建一個陣列 or 一個物件
let NewObject = (OriginalObject instanceof Array) ? [] : {};
// 遍歷 OriginalObject,並且判斷是 OriginalObject 的屬性才拷貝
for (let key in OriginalObject) {
if (OriginalObject.hasOwnProperty(key)) {
// 如果是物件自己的屬性,複製資料
NewObject[key] = OriginalObject[key];
}
}
return NewObject;
}
let Employee = ['Kay', 'Jay', () => {}];
let shallowCopyEmployee = customShallowCopy(Employee);
shallowCopyEmployee[1] = 'Shallow';
// [ 'Kay', 'Jay', [Function (anonymous)] ]
console.log(Employee);
// [ 'Kay', 'Shallow', null ]
console.log(shallowCopyEmployee);
Object 物件
使用 Object.assign() 複製
let Employee = {
name: 'Kay',
age: 17,
sayHi : () => {}
};
let directCopyEmployee = Employee;
let shallowCopyEmployee = Object.assign({}, Employee);
directCopyEmployee.name = 'Jay';
shallowCopyEmployee.name = 'Shallow';
// { name: 'Jay', age: 17, sayHi: [Function: sayHi] }
console.log(Employee);
// { name: 'Jay', age: 17, sayHi: [Function: sayHi] }
console.log(directCopyEmployee);
// { name: 'Shallow', age: 17, sayHi: [Function: sayHi] }
console.log(shallowCopyEmployee);
// true: reference 原始物件
console.log(Employee === directCopyEmployee);
// false: 建立全新物件
console.log(Employee === shallowCopyEmployee);
物件有 2 層以上結構,使用淺拷貝,改變第 2 層資料,會影響
原有物件
let Employee = {
name: 'Kay',
age: 17,
score : {
math : 30,
coding : 70,
},
};
let directCopyEmployee = Employee;
let shallowCopyEmployee = Object.assign({}, Employee)
directCopyEmployee.name = 'Jay';
// 改變第 1 層資料,不影響原有物件
shallowCopyEmployee.name = 'Shallow';
// 改變第 2 層資料,會影響原有物件
shallowCopyEmployee.score.math = 99999;
// {
// name: 'Jay',
// age: 17,
// score: { math: 99999, coding: 70 }
// }
console.log(Employee);
// {
// name: 'Jay',
// age: 17,
// score: { math: 99999, coding: 70 }
// }
console.log(directCopyEmployee);
// {
// name: 'Shallow',
// age: 17,
// score: { math: 99999, coding: 70 }
// }
console.log(shallowCopyEmployee);
// true: reference 原始物件
console.log(Employee === directCopyEmployee);
// false: 建立全新物件
console.log(Employee === shallowCopyEmployee);
// true: 第二層 reference 原始物件第 2 層
console.log(Employee.score === shallowCopyEmployee.score);
使用 {…Object} 展開運算元複製
let Employee = {
name: 'Kay',
age: 17,
sayHi : () => {}
};
let directCopyEmployee = Employee;
let shallowCopyEmployee = {...Employee};
directCopyEmployee.name = 'Jay';
shallowCopyEmployee.name = 'Shallow';
// { name: 'Jay', age: 17, sayHi: [Function: sayHi] }
console.log(Employee);
// { name: 'Jay', age: 17, sayHi: [Function: sayHi] }
console.log(directCopyEmployee);
// { name: 'Shallow', age: 17, sayHi: [Function: sayHi] }
console.log(shallowCopyEmployee);
// true: reference 原始物件
console.log(Employee === directCopyEmployee);
// false: 建立全新物件
console.log(Employee === shallowCopyEmployee);
使用 JSON.stringify() 及 JSON.parse() 複製
注意!
函數
不可以被拷貝
let Employee = {
name: 'Kay',
age: 17,
sayHi : () => {}
};
let directCopyEmployee = Employee;
let shallowCopyEmployee = JSON.parse( JSON.stringify(Employee));
directCopyEmployee.name = 'Jay';
shallowCopyEmployee.name = 'Shallow';
// { name: 'Jay', age: 17, sayHi: [Function: sayHi] }
console.log(Employee);
// { name: 'Jay', age: 17, sayHi: [Function: sayHi] }
console.log(directCopyEmployee);
// { name: 'Shallow', age: 17 }
console.log(shallowCopyEmployee);
// true: reference 原始物件
console.log(Employee === directCopyEmployee);
// false: 建立全新物件
console.log(Employee === shallowCopyEmployee);
自製淺拷貝函式
let customShallowCopy = function (OriginalObject) {
// 只拷貝物件
if (typeof OriginalObject !== 'object') {
return OriginalObject;
}
// 根據 OriginalObject 的類型,判斷要新建一個陣列 or 一個物件
let NewObject = (OriginalObject instanceof Array) ? [] : {};
// 遍歷 OriginalObject,並且判斷是 OriginalObject 的屬性才拷貝
for (let key in OriginalObject) {
if (OriginalObject.hasOwnProperty(key)) {
// 如果是物件自己的屬性,複製資料
NewObject[key] = OriginalObject[key];
}
}
return NewObject;
}
let Employee = {
name: 'Kay',
age: 17,
sayHi : () => {}
};
let directCopyEmployee = Employee;
let shallowCopyEmployee = customShallowCopy(Employee);
directCopyEmployee.name = 'Jay';
shallowCopyEmployee.name = 'Shallow';
// { name: 'Jay', age: 17, sayHi: [Function: sayHi] }
console.log(Employee);
// { name: 'Jay', age: 17, sayHi: [Function: sayHi] }
console.log(directCopyEmployee);
// { name: 'Shallow', age: 17, sayHi: [Function: sayHi] }
console.log(shallowCopyEmployee);
// true: reference 原始物件
console.log(Employee === directCopyEmployee);
// false: 建立全新物件
console.log(Employee === shallowCopyEmployee);
深拷貝 (Deep Copy) 方法
陣列 Array
自製深拷貝函式
當遇到多層的資料,會再各別進入該層進行 淺拷貝
let customDeepCopy = function (OriginalObject) {
// 只拷貝物件
if (typeof OriginalObject !== 'object') {
return OriginalObject;
}
// 根據 OriginalObject 的類型,判斷要新建一個陣列 or 一個物件
let NewObject = (OriginalObject instanceof Array) ? [] : {};
// 遍歷 OriginalObject,並且判斷是 OriginalObject 的屬性才拷貝
for (let key in OriginalObject) {
if (OriginalObject.hasOwnProperty(key)) {
if (typeof OriginalObject[key] === 'object') {
// 如果 OriginalObject 的子属性是物件,則再次深拷貝
NewObject[key] = customDeepCopy(OriginalObject[key]);
} else {
// 不是物件,直接拷貝
NewObject[key] = OriginalObject[key];
}
}
}
return NewObject;
}
let Employee = [
'Kay',
'Jay',
['Apple', 'Pen']
];
let directCopyEmployee = Employee;
let deepCopyEmployee = customDeepCopy(Employee);
directCopyEmployee[1] = 'KJ';
// 改變第 1 層資料,不影響原有物件
deepCopyEmployee[1] = 'Shallow';
// 改變第 2 層資料,會影響原有物件
deepCopyEmployee[2][0] = 'Pineapple';
// [ 'Kay', 'KJ', [ 'Apple', 'Pen' ] ]
console.log(Employee);
// [ 'Kay', 'KJ', [ 'Apple', 'Pen' ] ]
console.log(directCopyEmployee);
// [ 'Kay', 'Shallow', [ 'Pineapple', 'Pen' ] ]
console.log(deepCopyEmployee);
// true: reference 原始物件
console.log(Employee === directCopyEmployee);
// false: 建立全新物件
console.log(Employee === deepCopyEmployee);
// false: 第一層數值資料完整複製
console.log(Employee[1] === deepCopyEmployee[1]);
// false: 第 2 層建立全新物件
console.log(Employee[2] === deepCopyEmployee[2]);
使用 lodash 套件 cloneDeep()
const _ = require('lodash');
let Employee = [
'Kay',
'Jay',
['Apple', 'Pen']
];
let directCopyEmployee = Employee;
let deepCopyEmployee = _.cloneDeep(Employee);
directCopyEmployee[1] = 'KJ';
// 改變第 1 層資料,不影響原有物件
deepCopyEmployee[1] = 'Shallow';
// 改變第 2 層資料,不影響原有物件
deepCopyEmployee[2][0] = 'Pineapple';
// [ 'Kay', 'KJ', [ 'Apple', 'Pen' ] ]
console.log(Employee);
// [ 'Kay', 'KJ', [ 'Apple', 'Pen' ] ]
console.log(directCopyEmployee);
// [ 'Kay', 'Shallow', [ 'Pineapple', 'Pen' ] ]
console.log(deepCopyEmployee);
// true: reference 原始物件
console.log(Employee === directCopyEmployee);
// false: 建立全新物件
console.log(Employee === deepCopyEmployee);
// false: 第一層數值資料完整複製
console.log(Employee[1] === deepCopyEmployee[1]);
// false: 第 2 層建立全新物件
console.log(Employee[2] === deepCopyEmployee[2]);
物件 Object
自製深拷貝函式
當遇到多層的資料,會再各別進入該層進行 淺拷貝
let customDeepCopy = function (OriginalObject) {
// 只拷貝物件
if (typeof OriginalObject !== 'object') {
return OriginalObject;
}
// 根據 OriginalObject 的類型,判斷要新建一個陣列 or 一個物件
let NewObject = (OriginalObject instanceof Array) ? [] : {};
// 遍歷 OriginalObject,並且判斷是 OriginalObject 的屬性才拷貝
for (let key in OriginalObject) {
if (OriginalObject.hasOwnProperty(key)) {
if (typeof OriginalObject[key] === 'object') {
// 如果 OriginalObject 的子属性是物件,則再次深拷貝
NewObject[key] = customDeepCopy(OriginalObject[key]);
} else {
// 直接拷貝
NewObject[key] = OriginalObject[key];
}
}
}
return NewObject;
}
let Employee = {
name: 'Kay',
age: 17,
score : {
math : 30,
coding : 70,
},
};
let directCopyEmployee = Employee;
let deepCopyEmployee = customDeepCopy(Employee);
directCopyEmployee.name = 'Jay';
// 改變第 1 層資料,不影響原有物件
deepCopyEmployee.name = 'Shallow';
// 改變第 2 層資料,不影響原有物件
deepCopyEmployee.score.math = 99999;
// { name: 'Jay', age: 17, score: { math: 30, coding: 70 } }
console.log(Employee);
// { name: 'Jay', age: 17, score: { math: 30, coding: 70 } }
console.log(directCopyEmployee);
// { name: 'Shallow', age: 17, score: { math: 99999, coding: 70 } }
console.log(deepCopyEmployee);
// true: reference 原始物件
console.log(Employee === directCopyEmployee);
// false: 建立全新物件
console.log(Employee === deepCopyEmployee);
// false: 第 2 層建立全新物件
console.log(Employee.score === deepCopyEmployee.score);
使用 lodash 套件 cloneDeep()
const _ = require('lodash');
let Employee = {
name: 'Kay',
age: 17,
score : {
math : 30,
coding : 70,
},
};
let directCopyEmployee = Employee;
let deepCopyEmployee = _.cloneDeep(Employee)
directCopyEmployee.name = 'Jay';
// 改變第 1 層資料,不影響原有物件
deepCopyEmployee.name = 'Shallow';
// 改變第 2 層資料,不影響原有物件
deepCopyEmployee.score.math = 99999;
// { name: 'Jay', age: 17, score: { math: 30, coding: 70 } }
console.log(Employee);
// { name: 'Jay', age: 17, score: { math: 30, coding: 70 } }
console.log(directCopyEmployee);
// { name: 'Shallow', age: 17, score: { math: 99999, coding: 70 } }
console.log(deepCopyEmployee);
// true: reference 原始物件
console.log(Employee === directCopyEmployee);
// false: 建立全新物件
console.log(Employee === deepCopyEmployee);
// false: 第 2 層建立全新物件
console.log(Employee.score === deepCopyEmployee.score);
類別 class
自製 clone() 方法
class Cat {
constructor(name, color){
this.name = name;
this.color = color;
}
sayHi() {
console.log(`[Cat] My name is「${this.name}」 My color is 「${this.color}」`);
}
clone() {
return new Cat(this.name, this.color);
}
}
let KittyCat = new Cat('Kay', 'pink');
directCopyKittyCat = KittyCat;
deepCopyKittyCat = KittyCat.clone();
directCopyKittyCat.name = 'Jay';
deepCopyKittyCat.color = 'yellow';
// Cat { name: 'Jay', color: 'pink' }
console.log(KittyCat);
// Cat { name: 'Jay', color: 'pink' }
console.log(directCopyKittyCat);
// Cat { name: 'Kay', color: 'yellow' }
console.log(deepCopyKittyCat);
JSON.stringify() 及 JSON.parse() 複製產生的問題
- 函數 : 會連同 key 一起消失。
- undefined : 會連同 key 一起消失。
- NaN : 會被轉成 null。
- Infinity :會被轉成 null。
- regExp : 會被轉成 空 {}。
- Date : 型別會由 Data 轉成 string。
const originalData = {
// 資料會完全不見
undefined: undefined,
// 會被強制轉換成 null
notANumber: NaN,
// 會被強制轉換成 null
infinity: Infinity,
// 會強制轉換成空物件 {}
regExp: /.*/,
// 會變成字串
date: new Date('2022-01-01T23:59:59'),
// 資料會完全不見
sayHi : () => {}
};
const faultyClonedData = JSON.parse(JSON.stringify(originalData));
// {
// undefined: undefined,
// notANumber: NaN,
// infinity: Infinity,
// regExp: /.*/,
// date: 2022-01-01T15:59:59.000Z,
// sayHi: [Function: sayHi]
// }
console.log(originalData);
// {
// notANumber: null,
// infinity: null,
// regExp: {},
// date: '2022-01-01T15:59:59.000Z'
// }
console.log(faultyClonedData);
拷貝效能比較
效能比較原始測試文
- 測試時間:2020-04-30
- 測試平台
- Chrome v81.0
- Safari v13.1
- Firefox v75.0
- 作業系統:MacOs High Sierra v10.13.6.
拷貝方法
淺拷貝 Shallow Copy
編號 | 方法 |
---|---|
A | {...object} |
B | Object.assign() |
C | Object.key().reduce() |
D | Object.defineProperties() |
E | jQuery.extend({}, obj) |
F | lodash _.clone(obj) |
G | lodash _.clone(obj, true) |
H | lodash _.extend() |
I | customShallowCopy() |
深拷貝 Deep Copy
編號 | 方法 |
---|---|
J | lodash _.cloneDeep() |
K | JSON.parse() & JSON.stringify() |
L | jQuery.extend(true, {}, obj) |
M | obj.conttructor() |
N | EClone() |
O | obj.conttructor() |
P | Object.getOwnPropertyDescriptor() |
Q | handleDateArrayObject() |
R | __getDeepCircularCopy__ |
S | WeakMapCache() |
T | removeUniqueId |
U | copyPropDescs() |
拷貝方法 Benchmark 測試結果
淺拷貝 Shallow Copy 測試結果
方法 | 測試結果 |
---|---|
方法 A {…object} | 在 Chrome 及 Firefox 執行速度最快,在 Safari 執行速度中等 |
方法 B Object.assign() | 在所有瀏覽器執行速度相對都是較快 |
方法 E jQuery 及 方法 F,G,H lodash | 執行速度中等偏快 |
方法 K JSON.parse/stringify | 相當慢 |
方法 D, U | 在所有瀏覽器都很慢 |
深拷貝 Deep Copy 測試結果
方法 | 測試結果 |
---|---|
方法 Q | 在所有瀏覽器中都是最快的 |
方法 L jQuery 及方法 J lodash | 中等速度 |
方法 K JSON.parse/stringify | 相當慢 |
方法 U | 在所有瀏覽器都是最慢 |
方法 J lodash 及 方法 U | 當物件階層超過 1000 時,在 Chrome 會當掉 |
拷貝方法測試原始碼
參考資料
- JS 深拷貝與淺拷貝 | Explosion Units
- JavaScript 淺拷貝 (Shallow Copy) 與深拷貝 (Deep Copy) | Roya’s Blog
- JS 中的淺拷貝 (Shallow copy) 與深拷貝 (Deep copy) 原理與實作
- 關於JS中的淺拷貝(shallow copy)以及深拷貝(deep copy) | by Andy Chen | Andy的技術分享blog | Medium
- How do I correctly clone a JavaScript object? - Stack Overflow
- 透過複製陣列理解 JS 的淺拷貝與深拷貝 - JavaScript - Askie’s Coding Life