js 中的深浅拷贝是一个非常重要的知识点,因为在开发过程经常会遇到问题,特别是深拷贝。
至于什么是浅拷贝,什么是深拷贝这里不做解释,此外数组的浅拷贝可以使用 slice()
、concat()
函数,对象可以使用 Object.assign()
,这里也不过多介绍,再开发真正会遇到的问题在于深拷贝,下面我会列出几个方法以及各自的缺陷吧。
深拷贝就是指完全的拷贝一个对象,即使嵌套了对象,两者也相互分离,修改一个对象的属性,也不会影响另一个。
JSON.parse() 和 JSON.stringfy() 的运用
举个例子
var obj1 = {
member: ["Jack", "Rose", "Lucy"]
}
var obj2 = JSON.parse(JSON.stringify(obj1));
obj2.member[0] = "Paul";
console.log(obj1.member); // ['Jack', 'Rose', 'Lucy']
console.log(obj2.member); // ['Paul', 'Rose', 'Lucy']
方法很方便,JSON.parse(JSON.stringify(obj1))
将对象转为 json string,再转为对象,如果不在意下面提到的问题,也可以用这种方法,效率还是不错的。
问题1 丢失constructor
假如 obj1 由构造函数 Foo()
创建,那它的 constructor
也指向 Foo()
,同时 obj1 instanceof Foo
也为 true。
然后通过 var obj2 = JSON.parse(JSON.stringify(obj1));
拷贝出来的 obj2 的 constructor
指向 Object()
,当然 obj2 instanceof Foo
为 false。
问题2 内容丢失\改变
看个例子:
// 一个比较全的测试用例,下面的方法也会用到这个例子
function Foo() {
this.arr1 = [1, 2, 3];
this.arr2 = [1, undefined, null, function() {}];
this.obj1 = {a: 1, b: 2, c: 3};
this.obj2 = {a: undefined, b: null, c: function() {}};
this.unf = undefined;
this.nul = null;
this.func = function() {console.log("hello deepCopy")};
this.date = new Date(0);
this.reg = new RegExp('/^\w$/g');
this.err = new Error('ERROR!!!');
}
这是 new Foo()
的打印结果
这是 JSON.parse(JSON.stringify(new Foo() ))
的打印结果
直观点用个表格:
属性 | new Foo() | JSON.parse(JSON.stringify(new Foo()) |
---|---|---|
arr1 | [1, 2, 3] |
[1, 2, 3] |
arr2 | [1, undefined, null, f()] |
[1, null, null, null] |
date | Thu Jan 01 1970 08:00:00 GMT+0800 (中国标准时间) {} | “1970-01-01T00:00:00.000Z” |
err | Error: ERROR! | {} |
func | f() | 被忽略 |
obj1 | {a: 1, b: 2, c: 3} |
{a: 1, b: 2, c: 3} |
obj2 | {a: undefined, b: null, c: f()} |
{b: null} |
reg | /\/^w$\/g/ |
{} |
proto.constructor | Foo() | Object() |
我们可以发现只有 arr1、obj1
的结果是正确的,其他数据均有不同程度的失真,其中尤其是 arr2、obj2
的改变,与 JSON.stringify()
的特性有关,此外我们也可以看到上一个说的点 __proto__.constructor
变为了 Object()
。
关于JSON.stringify()
在《你不知道的JavaScript》中卷中有提到:
所有安全的 JSON 值(JSON-safe)都可以使用 JSON.stringify(..)
字符串化。安全的 JSON 值是指能够呈现为有效 JSON 格式的值。
为了简单起见,我们来看看什么是不安全的 JSON 值。undefined
、function
、symbol (ES6+)
和包含循环引用
(对象之间相互引用,形成一个无限循环)的对象都不符合 JSON 结构标准,支持 JSON 的语言无法处理它们。
JSON.stringify(..) 在对象中遇到 undefined、function 和 symbol 时会自动将其忽略,在数组中则会返回 null(以保证单元位置不变)。
这种方法比较适合平常开发中使用,因为通常不需要考虑对象和数组之外的类型。
递归实现深拷贝
直接列函数:
function deepCopy(obj) {
if (typeof obj === 'object') {
let newObj = obj instanceof Array ? [] : Object.create();
for(let key in obj) {
if(obj.hasOwnProperty(key)) {
newObj[key] = deepCopy(obj[key]);
}
}
return newObj;
}
return obj;
}
下面是递归实现对上面的测试用例的结果:
继续表格
属性 | new Foo() | deepCopy(new Foo()) |
---|---|---|
arr1 | [1, 2, 3] |
[1, 2, 3] |
arr2 | [1, undefined, null, f()] |
[1, undefined, {}, f()] |
date | Thu Jan 01 1970 08:00:00 GMT+0800 (中国标准时间) | Thu Jan 01 1970 08:00:00 GMT+0800 (中国标准时间) |
err | Error: ERROR! | Error: ERROR! |
func | f() | f() |
obj1 | {a: 1, b: 2, c: 3} |
{a: 1, b: 2, c: 3} |
obj2 | {a: undefined, b: null, c: f()} |
{a: undefined, b: {}, c: f()} |
reg | /\/^w$\/g/ |
/\/^w$\/g/ |
proto.constructor | Foo() | Foo() |
我们可以看到在 deepCopy()
中,已经对大部分的属性都能进行复制了,不过要看到在 arr2、obj2
对 null
的转换仍有问题,会把 null 复制成 {} 一个空对象。
并且我们在 deepCopy()
中用 Object.create()
取代 {}
来创建对象,使 __proto__.constructor
不丢失。
总结
一个完善的深拷贝是不好实现的,还有许多可完善的点:比如能拷贝自身可枚举、自身不可枚举、自身 Symbol 类型、原型上可枚举、原型上不可枚举、原型上 Symbol、循环引用等等,不过上面的方法在日常使用应该还是足够的。
有兴趣的可以看看下面几篇文章,当然也可以看看 JQuery、lodash 等库中对深拷贝的实现。