浅拷贝 (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)

参考资料