Fork me on GitHub

深拷贝/浅拷贝及实现方式

一、赋值

  • 基本数据类型:值传递,赋值后互不影响
1
2
3
4
5
6
let a = 'test';
let b = a;
console.log(b); // test

b = 'change';
console.log(a); // test
  • 引用数据类型:址传递,两个变量具有相同的引用,指向同一个内存地址,互相影响
1
2
3
4
5
6
7
8
9
10
11
12
13
let obj1 = {
name: 'bilibili',
info: {
age: 12,
height: '180cm'
}
}
let obj2 = obj1;
obj1.name = 'ohohoh'
console.log(obj2.name); // ohohoh

obj2.info.age = 100;
console.log(obj1.info.age); // 100

通常在开发中不希望改变变量a之后会影响变量b,因此需要用的深拷贝和浅拷贝

二、浅拷贝

概念

创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝
如果属性是基本类型,拷贝的就是值;如果属性是引用类型,拷贝的就是内存地址
简单来说,浅拷贝只解决了第一层的问题,拷贝第一层的基本类型值,以及第一层的引用类型地址。

使用场景

1.Object.assign()
Object.assign() 方法用于将所有可枚举属性的值从一个或多个源对象复制到目标对象,并且返回目标对象。

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
let obj1 = {
name: 'bilibili',
info: {
age: 12,
height: '180cm'
}
}
let obj2 = Object.assign({}, obj1);
console.log(obj2);
// {
// name: "bilibili",
// book: {age: 12, height: "180cm"}
// }

obj1.name = "ohohoh";
obj1.info.age = 100;
console.log(obj1);
// {
// name: "ohohoh",
// book: {age: 100, height: "180cm"}
// }

console.log(obj2);
// {
// name: "bilibili",
// book: {age: 100, height: "180cm"}
// }

2.展开语法 Spread
和Object.assign()效果相同

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
let obj1 = {
name: 'bilibili',
info: {
age: 12,
height: '180cm'
}
}
let obj2 = {
...obj1
};
console.log(obj2);
// {
// name: "bilibili",
// book: {age: 12, height: "180cm"}
// }

obj1.name = "ohohoh";
obj1.info.age = 100;
console.log(obj1);
// {
// name: "ohohoh",
// book: {age: 100, height: "180cm"}
// }

console.log(obj2);
// {
// name: "bilibili",
// book: {age: 100, height: "180cm"}
// }

3.Array.prototype.slice()
slice() 方法返回一个新的数组对象,这一对象是一个由 begin和 end(不包括end)决定的原数组的浅拷贝,原始类型数据不会被改变。

1
2
3
4
5
6
7
8
9
10
11
12
let a = [0, "1", [2, 3]];
let b = a.slice(1);
console.log(b);
// ["1", [2, 3]]

a[1] = "99";
a[2][0] = 4;
console.log(a);
// [0, "99", [4, 3]]

console.log(b);
// ["1", [4, 3]]

如何实现一个浅拷贝?

一个简单的实现:遍历对象属性

1
2
3
4
5
6
7
8
9
function shadowCopy(source) {
const res = {};
for (let i in source) {
if (source.hasOwnProperty(i)) {
res[i] = source[i];
}
}
return res;
}

⚠️深入探究:Object.assign()实现
实现思路:
1.判断原生 Object 是否支持该函数,如果不存在的话创建一个函数 assign,并使用Object.defineProperty 将该函数绑定到 Object 上。
2.判断参数是否正确(目标对象不能为空,我们可以直接设置{}传递进去,但必须设置值)。
3、使用 Object() 转成对象,并保存为 to,最后返回这个对象 to。
4、使用 for..in 循环遍历出所有可枚举的自有属性。并复制给新的目标对象(使用 hasOwnProperty 获取自有属性,即非原型链上的属性)。
5、此模拟实现不支持 symbol 属性。

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
if (typeof Object.assign2 !== 'function') {
Object.defineProperty(Object, 'assign2', {
value: function (target) {
// 开启严格模式。
'use strict';
// undefined == null 返回 true
if (target == null) {
throw new TypeError('Cannot convert undefined or null to object');
}
// 原始类型被包装为对象
let to = Object(target);
for (let i = 0; i < arguments.length; i++) {
let nextSource = arguments[i];
if (nextSource != null) {
for (let nextKey in nextSource) {
// 过滤其原型链上的属性
if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) {
to[nextKey] = nextSource[nextKey];
}
}
}
}
return to;
},
enumerable: false,
writable: true,
configurable: true
})
}

测试一下:

1
2
3
4
5
6
7
8
9
10
11
12
let a = { name: 123 };
let b = { age: 10, like: { apple: true } };
let c = Object.assign2(a, b);

console.log(a)
// {
// age: 10,
// like: { apple: true },
// name: 123
// }

a === c // true

⚠️关于以上代码的说明:
注意1:可枚举性
原生情况下挂载在 Object 上的属性是不可枚举的,但是直接在 Object 上挂载属性 a 之后是可枚举的,所以这里必须使用 Object.defineProperty,并设置 enumerable: false 以及 writable: true, configurable: true。

如何查看 Object.assign 是否可枚举?

两种方法:Object.getOwnPropertyDescriptor或者Object.propertyIsEnumerable

1
2
3
4
5
6
7
8
9
Object.getOwnPropertyDescriptor(Object, "assign");
// {
// value: ƒ assign(),
// writable: true, // 可写
// enumerable: false, // 不可枚举,注意这里是 false
// configurable: true // 可配置
// }

Object.propertyIsEnumerable("assign"); // false

再看直接在Object上挂载属性a

1
2
3
4
5
6
7
8
9
10
11
12
13
Object.a = function () {
console.log("log a");
}

Object.getOwnPropertyDescriptor(Object, "a");
// {
// value: ƒ (),
// writable: true,
// enumerable: true, // 注意这里是 true
// configurable: true
// }

Object.propertyIsEnumerable("a"); // true

通过Object.defineProperty挂载属性b, writable、enumerable、configurable默认都是false

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 通过
Object.defineProperty(Object, "b", {
value: function() {
console.log("log b");
}
});

Object.getOwnPropertyDescriptor(Object, "b");
// {
// value: ƒ (),
// writable: false,
// enumerable: false,
// configurable: false
// }

注意2:判断参数是否正确
因为 undefined 和 null 相等,undefined == null 返回 true,所以只需判断是否等于null即可

1
2
3
if (target == null) { // TypeError if undefined or null
throw new TypeError('Cannot convert undefined or null to object');
}

注意3:原始类型被包装为对象

1
2
3
4
5
6
7
8
9
10
var v1 = "abc";
var v2 = true;
var v3 = 10;
var v4 = Symbol("foo");

var obj = Object.assign({}, v1, null, v2, undefined, v3, v4);
// 原始类型会被包装,null 和 undefined 会被忽略。
// 注意,只有字符串的包装对象才可能有自身可枚举属性。
console.log(obj);
// { 0: "a", 1: "b", 2: "c" }

v2、v3、v4 实际上被忽略了,原因在于他们自身没有可枚举属性。

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
var v1 = "abc";
var v2 = true;
var v3 = 10;
var v4 = Symbol("foo");

var obj = Object.assign({}, v1, null, v2, undefined, v3, v4);
// 原始类型会被包装,null 和 undefined 会被忽略。
// 注意,只有字符串的包装对象才可能有自身可枚举属性。
console.log(obj);
// { "0": "a", "1": "b", "2": "c" }
复制代码上面代码中的源对象 v2、v3、v4 实际上被忽略了,原因在于他们自身没有可枚举属性。
// 木易杨
var v1 = "abc";
var v2 = true;
var v3 = 10;
var v4 = Symbol("foo");
var v5 = null;

// Object.keys(..) 返回一个数组,包含所有可枚举属性
// 只会查找对象直接包含的属性,不查找[[Prototype]]链
Object.keys( v1 ); // [ '0', '1', '2' ]
Object.keys( v2 ); // []
Object.keys( v3 ); // []
Object.keys( v4 ); // []
Object.keys( v5 ); // TypeError: Cannot convert undefined or null to object

// Object.getOwnPropertyNames(..) 返回一个数组,包含所有属性,无论它们是否可枚举
// 只会查找对象直接包含的属性,不查找[[Prototype]]链
Object.getOwnPropertyNames( v1 ); // [ '0', '1', '2', 'length' ]
Object.getOwnPropertyNames( v2 ); // []
Object.getOwnPropertyNames( v3 ); // []
Object.getOwnPropertyNames( v4 ); // []
Object.getOwnPropertyNames( v5 ); // TypeError: Cannot convert undefined or null to object

目标对象是原始类型,会包装成对象,对应上面的代码就是目标对象 a 会被包装成 [String: ‘abc’],那模拟实现时应该如何处理?使用 Object(..) 就可以了
⚠️注意: Object(“abc”) 时,其属性描述符为不可写,即 writable: false。

1
2
3
4
5
6
7
8
9
10
11
12
var myObject = Object( "abc" );

Object.getOwnPropertyNames( myObject );
// [ '0', '1', '2', 'length' ]

Object.getOwnPropertyDescriptor(myObject, "0");
// {
// value: 'a',
// writable: false, // 注意这里
// enumerable: true,
// configurable: false
// }
1
2
3
var a = "abc";
var b = "def";
Object.assign(a, b); // TypeError: Cannot assign to read only property '0' of object '[object String]'

⚠️ JS 对于不可写的属性值的修改静默失败(silently failed),在严格模式下才会提示错误,所以模拟实现要开启严格模式才会有相同效果报错。

注意4:存在性
补充知识:如何在不访问属性值的情况下判断对象中是否存在某个属性?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var anotherObj = {
a: 1
};
// 创建一个关联到 anotherObject 的对象
var myObj = Object.create( anotherObj );
myObj.b = 2;

myObj; // {b: 2}
anotherObj; // {a: 1}

("a" in myObj); // true
("b" in myObj); // true

myObj.hasOwnProperty('a'); // false
myObj.hasOwnProperty('b'); // true

⚠️ 这里涉及in操作符和hasOwnProperty 方法区别

  • in 操作符会检查属性是否在对象及其 [[Prototype]] 原型链中。
  • 、hasOwnProperty(..) 只会检查属性是否在 myObject 对象中,不会检查 [[Prototype]] 原型链。

Object.assign 方法肯定不会拷贝原型链上的属性,所以模拟实现时需要用 hasOwnProperty(..) 判断处理下,但是直接使用 myObject.hasOwnProperty(..) 是有问题的,因为有的对象可能没有连接到 Object.prototype 上(比如通过 Object.create(null) 来创建),这种情况下,使用 myObject.hasOwnProperty(..) 就会失败。

1
2
3
4
5
6
7
8
var myObject = Object.create( null );
myObject.b = 2;

("b" in myObject);
// true

myObject.hasOwnProperty( "b" );
// TypeError: myObject.hasOwnProperty is not a function

解决方法:call方法

1
2
3
4
5
var myObject = Object.create( null );
myObject.b = 2;

Object.prototype.hasOwnProperty.call(myObject, "b");
// true

三、深拷贝

概念

深拷贝会拷贝所有的属性,并拷贝属性指向的动态分配的内存。当对象和它所引用的对象一起拷贝时即发生深拷贝。深拷贝相比于浅拷贝速度较慢并且花销较大。拷贝前后两个对象互不影响。

使用场景

JSON.parse(JSON.stringify(object))

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
let obj1 = {
name: 'bilibili',
info: {
age: 12,
height: '180cm'
}
}
let obj2 = JSON.parse(JSON.stringify(obj1));
console.log(obj2)
// {
// name: "bilibili",
// book: {age: 12, height: "180cm"}
// }

obj1.name = "ohohoh";
obj1.info.age = 100;
console.log(obj1);
// {
// name: "ohohoh",
// book: {age: 100, height: "180cm"}
// }

console.log(obj2);
// {
// name: "bilibili",
// book: {age: 12, height: "180cm"}
// }

// 数组效果同上,改变数组不会影响拷贝之后的数组

⚠️ 这个方法存在以下几个问题:

1.会忽略undefined
2.忽略symbol类型
3.不能序列化函数

1
2
3
4
5
6
7
let obj = {
a: undefined,
b: Symbol('bilibili'),
c: function() {}
}
let obj1 = JSON.parse(JSON.stringify(obj));
console.log(obj1); // {}

4.不能解决循环引用的对象

1
2
3
4
5
6
7
8
9
10
11
let obj = {
a: 1,
b: {
c: 2,
d: 3
}
}
obj.a = obj.b;
obj.b.c = obj.a;
let obj1 = JSON.parse(JSON.stringify(obj));
console.log(obj1); // VM1334:1 Uncaught TypeError: Converting circular structure to JSON

5.不能正确处理new Date()

1
2
3
4
5
6
7
8
 new Date();
// Mon Dec 24 2018 10:59:14 GMT+0800 (China Standard Time)

JSON.stringify(new Date());
// "2018-12-24T02:59:25.776Z"

JSON.parse(JSON.stringify(new Date()));
// "2018-12-24T02:59:41.523Z"

解决方法:date转成字符串或者时间戳

1
2
3
4
5
6
7
8
let date = (new Date()).valueOf();
// 1545620645915

JSON.stringify(date);
// "1545620673267"

JSON.parse(JSON.stringify(date));
// 1545620658688

6.不能处理正则

1
2
3
4
5
6
7
8
9
10
11
12
13
let obj = {
name: "bilibili",
a: /'123'/
}

console.log(obj);
// {name: "muyiy", a: /'123'/}

let b = JSON.parse(JSON.stringify(obj));
console.log(b);

// {name: "bilibili", a: {}}
// 深拷贝的常用方法还有jQuery.extend() 和 lodash.cloneDeep()

思考:如何实现深拷贝?

参考文章:
你不知道的js(上)
掘金

本文标题:深拷贝/浅拷贝及实现方式

文章作者:tongtong

发布时间:2019年03月19日 - 20:03

最后更新:2019年05月05日 - 19:05

原始链接:https://ilove-coding.github.io/2019/03/19/深拷贝:浅拷贝及实现方式/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

坚持原创技术分享,您的支持将鼓励我继续创作!
-------------本文结束-------------