Featured image of post 【JavaScript 變數】淺拷貝 (Shallow Copy) 與深拷貝 (Deep Copy)

【JavaScript 變數】淺拷貝 (Shallow Copy) 與深拷貝 (Deep Copy)

【JavaScript 變數】淺拷貝 (Shallow Copy) 與深拷貝 (Deep Copy)

Photo by Mohammad Rahmani on Unsplash

資料傳遞方式

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)

參考資料

Donate KJ 贊助作者喝咖啡

如果這篇文章對你有幫助的話,可以透過下面支付方式贊助作者喝咖啡,如果有什麼建議或想說的話可以贊助並留言給我
If this article has been helpful to you, you can support the author by treating them to a coffee through the payment options below. If you have any suggestions or comments, feel free to sponsor and leave a message for me!
方式 Method 贊助 Donate
PayPal https://paypal.me/kejyun
綠界 ECPay https://p.ecpay.com.tw/AC218F1
歐付寶 OPay https://payment.opay.tw/Broadcaster/Donate/BD2BD896029F2155041C8C8FAED3A6F8
All rights reserved,未經允許不得隨意轉載
Built with Hugo
Theme Stack designed by Jimmy