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
主题 StackJimmy 设计