深浅拷贝实现(JavaScript)

数据类型

在了解深、浅拷贝之前,得先了解 JavaScript 中的数据类型和存储方式

image.png

  • 基本类型:numberstringbooleannullundefinedsymbol

    • 存储在栈内存中
    • 大小固定、体积轻量、相对简单
    • 赋值操作会直接生成一个新的值(深拷贝)
  • 引用类型:objectarrayfunction

    • 栈存储该对象的引用地址,实际数据存放在堆内存
    • 大小不定、占用空间较大、比较复杂
    • 赋值操作会直接将指针指向该实体的引用地址(处于节省内存考虑,属于浅拷贝)

深浅拷贝定义

浅拷贝:复制某个对象的指针地址,而不是复制对象本身,新旧对象依然共享一块内存

深拷贝:创造一个一模一样的对象,新对象与原对象不共享内存,修改新对象不影响到原对象

浅拷贝

Array

slice()

1
2
3
4
5
6
7
8
9
function cloneArrayBySlice(list){
return list.slice()
}
var arr = [1, 2, { name: 'xy' }]
var arrClone = cloneArrayBySlice(arr)
arr[0] = 2
arrClone[0] = 3
arrClone[2].name = 'change' // 引用类型是浅拷贝地址
console.log(arr, arrClone) // [ 2, 2, { name: 'change' } ] [ 3, 2, { name: 'change' } ]

concat

1
2
3
function cloneArrayByConcat(list){
return [].concat(list)
}

spread

1
2
3
function cloneArrayBySpread(list){
return [...list]
}

Object

assign

值得注意的是 assign 接受第一个入参为 target 后续的参数为混入到 target 上,所以也算一种浅拷贝吧(勉强)

1
2
3
function assign(obj){
return Object.assign(obj)
}

深拷贝

assign

为什么这里还能出现在这个?

Object.assign 方法只会拷贝源对象自身的并且可枚举的属性到目标对象

1
2
var a = { a: 1 }
var b = Object.assign({}, a) // 此时是将 a 混入到 {} 对象上,所以 a != b

当然它只要超过值是引用类型还是执行浅拷贝

1
2
3
var a = { a: 1, b: { b : 2 } }
var b = Object.assign({}, a)
a.b === b.b

JSON.parse

这个方法的原理是将对象序列化成字符串,再解析 json 字符串解析为对象

1
2
3
function clone(obj){
return JSON.parse(JSON.stringify(obj))
}

缺点

  • 忽略 symbol undefined function 类型
  • 不支持循环引用对象的拷贝

image.png

MessageChannel

Channel Messaging API的 MessageChannel 接口允许我们创建一个新的消息通道,并通过它的两个MessagePort 属性发送数据

1
2
3
var { port1, port2 } = new MessageChannel()
port1.onmessage = e => console.log(e.data)
port2.postMessage('hello from port2') // hello from port2
1
2
3
4
5
6
7
function clone(obj){
return new Promise((res, rej) => {
const { port1, port2 } = new MessageChannel()
port1.onmessage = e => res(e.data)
port2.postMessage(obj)
})
}

缺点

  • 异步取值
  • 不支持 functionsymbol 类型

image.png

image.png

递归遍历

使用递归去创建对象,再赋值,可以处理大部分场景

1
2
3
4
5
6
7
8
9
10
11
12
function clone(obj) {
if(!obj) return null
var target = Array.isArray(obj) ? [] : {}
for(var key in obj){
if(typeof obj[key] === 'object'){
target[key] = clone(obj[key])
}else{
target[key] = obj[key]
}
}
return target
}

缺点

  • 不支持循环引用

image.png

处理循环引用的解决方案

使用 WeakMap 数据结构存储循环引用的映射关系

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function clone(obj){
var map = new WeakMap()
function _clone(obj){
var val = map.get(obj)
if(val) return val
var target = obj instanceof Array ? [] : {}
map.set(obj, target)
for (const key in obj) {
if (Object.hasOwnProperty.call(obj, key)) {
const el = obj[key];
if(el && typeof el === 'object') {
target[key] = _clone(el)
}else{
target[key] = el
}
}
}
return target
}
return _clone(obj)
}

测试用例

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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
const { clone } = require('./clone.js')
const runTests = (tests, message) => {
describe(message, function() {
tests.forEach(([args, res], i) => {
test(`${i}`, () => {
expect(clone(args)).toEqual(args)
});
})
})
}
const Tests = [
{
tests: [
[
(function () {
var a = {
a: 1,
b: '1',
c: true,
d: false,
e: null,
f: undefined,
g: function(){},
h: Symbol('b'),
i: [
1, '1', true, false, null, undefined, function(){},
{
a: 1,
b: '1',
c: true,
d: false,
e: null,
f: undefined,
g: function(){},
h: Symbol('b'),
},
[1, '1', true, false, null, undefined, function(){}]
],
j: {
a: 1,
b: '1',
c: true,
d: false,
e: null,
f: undefined,
g: function(){},
h: Symbol('b'),
i: [
1, '1', true, false, null, undefined, function(){},
{
a: 1,
b: '1',
c: true,
d: false,
e: null,
f: undefined,
g: function(){},
h: Symbol('b'),
}
]
},
}
return {
...a,
a
}
})()
],
],
message: 'number'
}
]

Tests.forEach(({tests, message}) => runTests(tests, message))

image.png

源码地址