深拷贝系列 ———— 什么是深拷贝、浅拷贝、Object.assign

简介

首先我们要了解什么是深拷贝浅拷贝,要了解深拷贝/浅拷贝首先要了解浅拷贝JavaScript中的数据类型。

JavaScript中的数据类型分为两类:

  • 值类型/原始类型/基本类型:String、Number、Boolean、null、undefined、Symbol
  • 引用类型/“指针”类型:Object、Array、Window等等

基本类型是储存在栈(stack)中的数据。
引用类型真实数据是储存在堆中的,而它的引用地址储存在栈中。

深拷贝/浅拷贝

如果有兴趣了解JavaScript中的类型的话,可以看一下我往期的文章JavaScript 数据类型(一) 常见数据类型,这个只是其中的一篇,有关JavaScript类型的常见的概念基本上都有提及。

基本类型是不存在深拷贝浅拷贝的,因为基本类型是不可变的,无论是修改重新赋值赋值给别的变量都是一个新的值,和原来的值再无关联。

1
2
3
4
var a = 'abc',
b = a;
b = 'abcd';
console.log(a, b); // abc, abcd

引用类型因为是地址引用,所以会存在深拷贝浅拷贝,下面就开始介绍。

  • 浅拷贝简介: 创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值,如果属性是引用类型,拷贝的就是内存地址 ,所以如果其中一个对象改变了这个地址,就会影响到另一个对象。

深拷贝/浅拷贝

  • 深拷贝: 创建一个新的对象把原始对象的所有属性的拷贝一份,并且引用类型的引用地址和内存空间都会被拷贝一份,重新分配内存空间修改新对象不会影响原始对象

深拷贝/浅拷贝

本章主要记录深拷贝/浅拷贝,主要讲解浅拷贝相关的如assign、解构、扩展运算符、slice等等,后面的文章会由浅到深的的介绍深拷贝相关的。

浅拷贝

虽然看着浅拷贝比较简单,其实它就是比较简单,但是它相关的东西也不少,下面就开始吧。

  • Object.assign() & 自己实现一个 Object.assign()
  • Array.prototype.slice()
  • Array.prototype.concat()
  • 解构
  • …扩展运算符
  • jquery.extend()
  • 自己实现一个浅拷贝

Object.assign()

语法: Object.assign(target, ...sources);
返回值: target对象

ES6 中拷贝对象的方法,接受的第一个参数是拷贝的目标target,剩下的参数是拷贝的源对象sources可以是多个

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var target = { firstname: 'target', age: 20 };
var source = { lastname: 'source', age: 21 };
const newtarget = Object.assign(target, source);
// target与newtarget指向同一个内存地址
console.log(target); // {firstname: "target", age: 21, lastname: "source"}
console.log(newtarget); // {firstname: "target", age: 21, lastname: "source"}
console.log(newtarget === target); // true

// 修改newtarget的age属性,target属性也跟着变化,而source不会变化
newtarget.age = 22;
console.log(target); // {firstname: "target", age: 22, lastname: "source"}
console.log(newtarget); // {firstname: "target", age: 22, lastname: "source"}
console.log(source); // { lastname: 'source', age: 21 }

// 通过Object.assgin()第一个传入一个空对象,结果和上方相同
const newSource = Object.assign({}, source);
console.log(newSource); // {firstname: "target", age: 21, lastname: "source"}

newSource.age = 22;
console.log(source); // {firstname: "target", age: 21, lastname: "source"}
console.log(newSource); // {firstname: "target", age: 22, lastname: "source"}

在上面的代码中我们可以看到通过Object.assign(target, source);会返回一个新的值newtarget,这个值的引用地址与target是同一个地址,所以修改newtarget.age = 22;targetnewtarget都会变化。
Object.assign({}, source)返回的newSource它被修改不会影响到source的变化,因为他的target传入的是一个空对象。

自己实现一个 assgin

实现目标

  • 第一个target
  • 支持多个对象合并
  • 与 Object.assign 表现一至

大致分为下面几步:

  • 判断传入target如果不为对象,或者 传入为null时直接返回Object(target)
  • 获取所有参数,参数列表转为Array类型
  • 循环上一步生成数组,获取每一个传入的对象
  • 通过 for...in循环上一步获取的对象,并且通过hasOwnProperty判断当前属性是否是本身上的属性(不是原型上的)
  • 上一步通过判断的属性,赋值给target对象
  • 最后返回target对象

函数版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// 函数版本
function assign(target) {
// 验证第一个参数是否为object
if (typeof target !== 'object' || target == null) {
return Object(target);
}
// arguments转为数组
let copyList = Array.prototype.slice.call(arguments, 1);
let len = copyList.length;
// 循环复制多个对象的属性
for (let i = 0; i < len; i++) {
let item = copyList[i];
// 获取当前对象的属性
for (key in item) {
// 判断属性是否在对象本身上
if (item.hasOwnProperty(key)) {
// 复制给目标对象
target[key] = item[key];
}
}
}
// 返回目标对象
return target;
}
// 验证assign代码
var target = { firstname: 'target', age: 20 };
var source = { lastname: 'source', age: 21 };
const newtarget = Object.assign(target, source);
// target与newtarget指向同一个内存地址
console.log(target); // {firstname: "target", age: 21, lastname: "source"}
console.log(newtarget); // {firstname: "target", age: 21, lastname: "source"}
console.log(newtarget === target); // true

// 修改newtarget的age属性,target属性也跟着变化,而source不会变化
newtarget.age = 22;
console.log(target); // {firstname: "target", age: 22, lastname: "source"}
console.log(newtarget); // {firstname: "target", age: 22, lastname: "source"}
console.log(source); // { lastname: 'source', age: 21 }

// 通过Object.assgin()第一个传入一个空对象,结果和上方相同
const newSource = Object.assign({}, source);
console.log(newSource); // {firstname: "target", age: 21, lastname: "source"}

newSource.age = 22;
console.log(source); // {firstname: "target", age: 21, lastname: "source"}
console.log(newSource); // {firstname: "target", age: 22, lastname: "source"}

Object.defineProperty版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
if (typeof Object.newAssign !== 'function') {
Object.defineProperty(Object, 'newAssign', {
writable: true,
configurable: true,
enumberable: false,
value: function (target) {
// 验证第一个参数是否为object
if (typeof target !== 'object' || target == null) {
return Object(target);
}
// arguments转为数组
let copyList = Array.prototype.slice.call(arguments, 1);
let len = copyList.length;
// 循环复制多个对象的属性
for (let i = 0; i < len; i++) {
let item = copyList[i];
// 获取当前对象的属性
for (key in item) {
// 判断属性是否在对象本身上
if (item.hasOwnProperty(key)) {
// 复制给目标对象
target[key] = item[key];
}
}
}
// 返回目标对象
return target;
}
});
}

// 测试代码
Object.newAssign('abc', false); // String {"abc"}
Object.assign('abc', false); // String {"abc"}

Object.newAssign({}, 'abc'); // {0: "a", 1: "b", 2: "c"}
Object.assign({}, 'abc'); // {0: "a", 1: "b", 2: "c"}

Object.newAssign({}, 'abc', false, 123); // {0: "a", 1: "b", 2: "c"}
Object.assign({}, 'abc', false, 123); // {0: "a", 1: "b", 2: "c"}

Array.prototype.slice()

slice() 方法返回一个新的数组对象,这一对象是一个由 beginend(不包括 end)决定的原数组的浅拷贝。原始数组不会被改变。
slice() 它的定义其实是复制一个数组。

1
2
3
4
5
6
7
8
9
10
11
12
let old = ['a', 'b', ['c', 'd']];
// 通过slice复制当前数组
let newValue = old.slice(0);
console.log(newValue); // ['a', 'b', ['c', 'd']];

// 修改原数组的下标为0的值,新对象没有受到影响,因为下标为0的是基本类型,它们的内存地址是不同的
old[0] = 'f';
console.log(newValue); // ['a', 'b', ['c', 'd']];

// 修改原数组的下标为2的值,新对象受到影响,因为下标为2的是引用类型,它们的值指向同一个内存地址
old[2][0] = '';
console.log(newValue); // ['a', 'b', ['', 'd']];

Array.prototype.concat()

concat() 方法用于合并两个或多个数组。此方法不会更改现有数组,而是返回一个新数组

语法

var new_array = old_array.concat(value1[, value2[, ...[, valueN]]])

返回值
新的 Array 实例。

1
2
3
4
5
6
7
8
9
10
11
12
var old = ['a', 'b', ['c', 'd']];
// 通过concat返回一个新的Array实例
var newArr = Array.prototype.concat([], old);
console.log(newArr); // ['a', 'b', ['c', 'd']];

// 修改原数组的下标为0的值,新对象没有受到影响,因为下标为0的是基本类型,它们的内存地址是不同的
old[0] = 'f';
console.log(newArr); // ['a', 'b', ['c', 'd']];

// 修改原数组的下标为2的值,新对象受到影响,因为下标为2的是引用类型,它们的值指向同一个内存地址
old[2][0] = '';
console.log(newArr); // ['a', 'b', ['', 'd']];

concat()它的效果是和slice()相同的。

解构

解构ES6中的新特性,它可以方便的浅复制一个对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 声明一个对象
var old = {
name: 'old',
attr: {
age: 18,
sex: 'man'
}
};

// 通过解构复制attr出来,它只复制了内存地址
var { attr } = old;
console.log(attr); // {age: 18, sex: "man"}

// 修改原对象的attr值,新对象受到影响
old.attr.age = 20;
console.log(attr); // {age: 20, sex: "man"}

…扩展运算符

...扩展运算符也是ES6中的新特性,它可以方便的浅复制一个对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 声明一个对象
var old = {
name: 'old',
attr: {
age: 18,
sex: 'man'
}
};

// 通过...扩展运算符,它只复制了内存地址
var newValue = { ...old };
console.log(newValue); // {name: "old", attr: {age: 18, sex: "man"}}

// 修改原对象的attr.age值,新对象受到影响
old.attr.age = 20;
console.log(newValue); // {name: "old", attr: {age: 20, sex: "man"}}

jquery.extend

jquery.extend()是一个浅拷贝,这个在这里就不多做赘述了,如果想看实现原理的话,可以去看jquerygithub 上的源码实现。

自己实现一个浅拷贝

实现一个浅拷贝其实很简单,大致步骤如下:

  • 声明一个新对象
  • 旧对象的属性赋值给新对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// 声明函数
function shallowCopy(oldObj) {
// 声明新对象
var newObj = {};
// 判断传入是否为对象
if (typeof oldObj !== 'object') {
console.log('请传入对象');
return oldObj;
}
// 循环获取传入对象属性
for (key in oldObj) {
// 判断属性是否在对象本身上
if (oldObj.hasOwnProperty(key)) {
// 把属性复制给新对象
newObj[key] = oldObj[key];
}
}
// 返回新对象
return newObj;
}

// 声明一个对象
var old = {
name: 'old',
attr: {
age: 18,
sex: 'man'
}
};
// 通过shallowCopy
var newValue = shallowCopy(old);
console.log(newValue); // {name: "old", attr: {age: 18, sex: "man"}}

// 修改原始对象的基本类型属性,新对象不受影响
old.name = 'new';
console.log(newValue); // {name: "old", attr: {age: 18, sex: "man"}}

// 修改原始对象引用类型属性,新对象受到影响
old.attr.age = 20;
console.log(newValue); // {name: "old", attr: {age: 20, sex: "man"}}

总结

在本文中我们分别介绍了深拷贝浅拷贝是什么。着重介绍了我们日常使用的浅拷贝,并且自己实现了一个assign()和一个shallowCopy来加深对浅拷贝的理解,下一篇文件会介绍一个深拷贝相关的JSON.stringify()/JSON.parse()并且自己实现一个。

通过上面的实例,可以验证我们对浅拷贝的理解是对的,如果是基本类型浅拷贝可以把它的值拷贝到新对象中,如果是引用类型浅拷贝只能拷贝引用类型的引用地址。

参考

https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Array/slice
一文搞懂 JS 中的赋值·浅拷贝·深拷贝