淺拷貝 (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) 與深拷貝 (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 測試結果

前往淺拷貝 Shallow Copy 測試原始碼

方法 測試結果
方法 A {…object} ChromeFirefox 執行速度最快,在 Safari 執行速度中等
方法 B Object.assign() 在所有瀏覽器執行速度相對都是較快
方法 E jQuery 及 方法 F,G,H lodash 執行速度中等偏快
方法 K JSON.parse/stringify 相當慢
方法 D, U 在所有瀏覽器都很慢

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

深拷貝 Deep Copy 測試結果

前往深拷貝 Deep Copy 測試原始碼

方法 測試結果
方法 Q 在所有瀏覽器中都是最快的
方法 L jQuery 及方法 J lodash 中等速度
方法 K JSON.parse/stringify 相當慢
方法 U 在所有瀏覽器都是最慢
方法 J lodash 及 方法 U 當物件階層超過 1000 時,在 Chrome 會當掉

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

拷貝方法測試原始碼

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

參考資料