curTain

写一个深拷贝,考虑数组、对象和循环引用。

1.准备两个对象

  • 无循环引用的对象
1
2
3
4
5
6
7
8
9
10
11
12
13
var target = {
field1: "string",
field2: 33,
field3: undefined,
field4: null,
field5: {
child: "child"
},
field6: [ 1, 2, 3, 4 ],
field7: function(){
console.log( this.field1 )
}
}
  • 有循环引用的对象
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    var target = {
    field1: "string",
    field2: 33,
    field3: undefined,
    field4: null,
    field5: {
    child: "child"
    },
    field6: [ 1, 2, 3, 4 ],
    field7: function(){
    console.log( this.field1 )
    }
    }
    target.target = target

2.使用 JSON.parse(JSON.stringify())

2.1复制一个无循环引用的对象,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var target = {
field1: "string",
field2: 33,
field3: undefined,
field4: null,
field5: {
child: "child"
},
field6: [ 1, 2, 3, 4 ],
field7: function(){
console.log( this.field1 )
}
}
var res = JSON.parse( JSON.stringify( target ) )
console.log( "target: ", target )
console.log( "res: ", res )

得到的结果是:

总结:可见,JSON.stringify() 不能处理undefined和function。

2.2复制一个循环应用的对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var target = {
field1: "string",
field2: 33,
field3: undefined,
field4: null,
field5: {
child: "child"
},
field6: [ 1, 2, 3, 4 ],
field7: function(){
console.log( this.field1 )
}
}
target.target = target
var res = JSON.parse( JSON.stringify( target ) )
console.log( "target: ", target )
console.log( "res: ", res )

结果:

总结:可见,JSON.stringify() 不能处理处理循环引用的结构

3.手写一个深拷贝

3.1浅拷贝

1
2
3
4
5
6
7
function clone( target ){
let cloneTarget = {}
for( let key in target ){
cloneTarget[key] = target[key]
}
return cloneTarget
}

上面是一个浅拷贝,没有考虑数组与对象。

3.2 考虑对象

我们可以使用 typeof 来判断数据类型:

typeof可以查看数据的类型有 7 种:
typeof 12345 === number
typeof “999” === string
typeof {} === object null []
typeof undefined === undefined
typeof true === boolean
typeof symbol(23) === symbol
typeof console.log === function

typeof小提示

使用递归处理引用对象的拷贝:

1
2
3
4
5
6
7
8
9
10
11
function clone( target ){
if( typeof target === "object" ){
let cloneTarget = {}
for( let key in target ){
cloneTarget[key] = clone( target[key] )
}
return cloneTarget
} else {
return target
}
}

结果:

总结

因为typeof会把数组识别成object,所以,代码还需要再度改进。

3.3 考虑数组

1
2
3
4
5
6
7
8
9
10
11
function clone( target ){
if( typeof target === "object" ){
let cloneTarget = Array.isArray( target ) ? [] : {}
for( let key in target ){
cloneTarget[key] = clone( target[key] )
}
return cloneTarget
} else {
return target
}
}

测试结果:

总结:

可见,数组也可以复制了,下面我们要解决循环引用。

3.4 考虑循环引用

当学习过设计模式后,我们会发现,有很多情况存在循环引用的问题,所以,在深拷贝时,循环引用的问题是必须要解决的。

解决循环引用问题,我们可以额外开辟一个存储空间,来存储当前对象和拷贝对象的对应关系,当需要拷贝当前对象时,先去存储空间中找,有没有拷贝过这个对象,如果有的话直接返回,如果没有的话继续拷贝,这样就巧妙化解的循环引用的问题。

—引用自:如何写出一个惊艳面试官的深拷贝?

es6中,有这样一种数据结构叫 Map,他能将对象作为键,这样,我们就能很好的保存对象,并判断对象是否被拷贝过了。

整体流程如下:

  1. 是对象,检查map中是否存在此对象
  2. 存在—-返回保存在map中的键
  3. 不存在—将值存入map,键的值使用当前cloneTarget(这里其实用什么都行)
  4. 继续递归拷贝,并传入此时的map

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function clone( target, map = new Map() ){
if( typeof target === "object" ){
var cloneTarget = Array.isArray( target ) ? [] : {}
if( map.get( target ) ){
return map.get( target )
}
map.set( target, cloneTarget )
for( let key in target ){
cloneTarget[key] = clone( target[key], map )
}
return cloneTarget
} else {
return target
}
}

测试结果:

可以看见,已经解决了循环引用的问题。

3.5 简单性能优化

使用 WeakMap 数据结构,将复制对象属性的强引用变为弱引用。

WeakMap的作用,请参考 es6 WeakMap.

修改后的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function clone( target, map = new WeakMap() ){
if( typeof target === "object" ){
var cloneTarget = Array.isArray( target ) ? [] : {}
if( map.get( target ) ){
return map.get( target )
}
map.set( target, cloneTarget )
for( let key in target ){
cloneTarget[key] = clone( target[key], map )
}
return cloneTarget
} else {
return target
}
}

最后,我们就完成了一个简单的深拷贝,解决了对象、数组、循环引用的问题。

结语

总结一下写出一个深拷贝的过程:

  1. 需要传入两个参数,一个是拷贝目标,一个是作为拷贝过属性的容器 WeakMap
  2. 判断拷贝目标是否一个对象 或是 数组
  3. 是 使用 WeakMap 判断是否已经拷贝,并递归拷贝每个属性
  4. 不是 直接返回此属性的值

js中还有很多其他的数据结构,比如 Set 和 Map,这里就不做继续的深入了,

如果你想继续深入深拷贝,你可以参考:如何写出一个惊艳面试官的深拷贝?

参考

如何写出一个惊艳面试官的深拷贝?

阮一峰 es6


 评论