JavaScript 基础知识(2)—— 浅拷贝 与 深拷贝

浅拷贝和深拷贝只针对于像Object,Array这样的复杂对象。之所以会出现深浅拷贝,实质上是因为在JavaScript当中基础类型和引用类型的不同存储方式,可见上篇。由于对对象的操作实际上是在操作对象的引用,所以在复制一个引用类型时,复制的值并不是对象本身,其实也是对象的引用,即指向该对象的指针。

1
2
3
4
5
6
7
// 引用类型的复制
let me = { name: '张瑞', hobbies: ['游戏','旅游'] }
let myCopy = me
console.log(JSON.stringify(myCopy)) // {name:"张瑞",hobbies:["游戏","旅游"]}
me.hobbies.pop()
me.name = "张瑞瑞"
console.log(JSON.stringify(myCopy)) // {name:"张瑞瑞",hobbies:["游戏"]}

可以看出,复制的了我的人,复制不了我的心,由于me是个引用类型,在me改变内部属性时,myCopy中的属性也会也都会被改变。

浅拷贝

浅拷贝只复制对象内的第一层属性,结果对象可能会与源对象还保持着联系。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
let me = { name: '张瑞', hobbies: ['游戏','旅游'] }
function shallowCopy(obj) {
let _obj = {};
for (key in obj) {
// 只复制第一层属性
if (obj.hasOwnProperty(key)) {
_obj[key] = obj[key];
}
}
return _obj;
}
let myCopy = shallowCopy(me)
console.log(JSON.stringify(myCopy)) // {name:"张瑞",hobbies:["游戏","旅游"]}
me.hobbies.pop()
me.name = "张瑞瑞"
console.log(JSON.stringify(myCopy)) // {name:"张瑞",hobbies:["游戏"]}

由于只拷贝了第一层属性,在me改变name这个基本类型的属性时,并不会影响myCopy中的name属性。而改变hobbies这个引用类型的属性时,myCopy中的hobbies也被改变了,这就是浅拷贝:只复制对象内的第一层属性

ES6中新增的Object.assign方法便是一个浅拷贝的实践。用于对象的合并,将源对象(source)的所有可枚举属性,复制到目标对象(target)

通过浅拷贝得到的myCopy的hobbies属性和me的hobbies属性在内存中指向同一个地址,如果想完全的复制一个没有关联的我,这显然并不是期望得到的效果。所以,也就有了 深拷贝。

深拷贝

深拷贝可以对对象内的属性进行递归复制,拷贝后的新对象与源对象完全断开了联系。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
let me = { name: '张瑞', hobbies: { game:['LOL','吃鸡'], other:[] },like:{} }
function deepCopy(obj) {
let _obj = Array.isArray(obj) ? [] : {};
for (key in obj) {
// 还有 typeof obj[key] === 'function',例子暂不需要
if(typeof obj[key] === 'object'){
_obj[key] = deepCopy(obj[key])
}else {
_obj[key] = obj[key];
}
}
return _obj;
}
let myCopy = deepCopy(me)
console.log(JSON.stringify(myCopy)) // {"name":"张瑞","hobbies":{"game":["LOL","吃鸡"],"other":[]},"like":{}}
me.hobbies.game.pop()
me.name = "张瑞瑞"
console.log(JSON.stringify(me)) // {"name":"张瑞瑞","hobbies":{"game":["LOL"],"other":[]},"like":{}}
console.log(JSON.stringify(myCopy)) // {"name":"张瑞","hobbies":{"game":["LOL","吃鸡"],"other":[]},"like":{}}

不难看出,经过递归后,deepCopy完全的复制了另一个我。
在真正投入使用的时候,由于递归的特性,也会造成很多副作用,如对象环的问题(对象的某个属性值是对象本身),当递归调用次数足够大,就会造成栈溢出。下面有解决办法

其实还有更多的方法,如下

Reflect

Reflect 其实类似于 上文中的 for in 写法。使用Reflect.ownKeys

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 判断目标是否是对象
function isObject(val) {
return val != null && typeof val === 'object' && Array.isArray(val) === false;
};
function deepCopy(obj) {
if (!isObject(obj)) {
throw new Error('obj 不是一个对象!')
}
let copy = Array.isArray(obj) ? [...obj] : { ...obj }
Reflect.ownKeys(copy).forEach(key => {
copy[key] = isObject(obj[key]) ? deepCopy(obj[key]) : obj[key]
})
return copy
}

lodash jQuery.extend

lodash 中的 cloneDeep 等api更加完善,具体可以参考lodash的baseClone等方法

1
let copy = _.cloneDeep(me)

jQuery.extend也是非常经典的一个方法,用法多样。可以查看源码jQuery.extend

1
2
3
4
5
6
7
8
9
10
11
let object1 = {
apple: 0,
banana: {weight: 52, price: 100},
cherry: 97
};
let object2 = {
banana: {price: 200},
durian: 100
};
/* object2 合并到 object1 中 */
$.extend(object1, object2);

JSON 序列化反序列化

这可能也是最简单也是最好理解的方法了,但是在实际日常使用中也会出现一些问题,那是因为JSON语言中并没有undefined,function等数据类型,在拷贝具有这些类型的对象时,也必然会报错。

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

特殊情况

对象环问题

1
2
3
let obj1 = {}
obj1.self=obj1
let obj2 = deepCopy(obj1)//Uncaught RangeError: Maximum call stack size exceeded

对于这种问题,我们可以创建一个变量,在递归时把每个要拷贝的属性都缓存进来,下一轮递归的时候,如果缓存中有存在相同的对象,就可以直接使用这个对象并停止递归。这样改造之后,便不会在进入递归死循环,也就不会发生栈溢出了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function deepCopy(obj) {
let cache = []
cache.push(obj);
let _obj = Array.isArray(obj) ? [] : {};
for (key in obj) {
if(typeof obj[key] === 'object'){
// 使用indexOf可以检测数组中是否存在相同的对象
let index = cache.indexOf(obj[key]);
if (index > -1) {
_obj[key] = cache[index];
}else {
_obj[key] = deepCopy(obj[key])
}
}else {
_obj[key] = obj[key];
}
}
return _obj;
}
let obj1 = {}
obj1.self=obj1
let obj2 = deepCopy(obj1)// {self: {…}}

经过改造,没有再报栈溢出的错误,也得到了我们想要的结果。

当然,我们也可是使用WeakMap的方式创建一个更有趣的方法。

1
2
3
4
5
6
7
8
9
10
11
function deepCopy(obj, map=new WeakMap()){
if (!isObject(obj)) return obj;
if (map.has(obj)) return map.get(obj); // 有则返回
let copy = Array.isArray(obj) ? [] : {}
map.set(obj, copy) // 无则设置
return Object.assign(copy, Object.keys(obj).map(key => ({[key]:deepCopy(obj[key], map)})))
}
let me = {name:'张瑞'}
me.like=me
let he = deepCopy(me)
console.log(he) // {name: "张瑞", like: {…}}

WeakMap的键名所指向的对象,不计入垃圾回收机制。WeakMap的设计目的在于,有时我们想在某个对象上面存放一些数据,但是这会形成对于这个对象的引用。
由于WeakMap的特性,它的键是弱引用的,正好符合现在的需求。

lodash也能处理对象环问题,具体可参见源码baseClone#L198

拷贝原型上的属性

由于在JavaScript中,对象是基于原型链设计的,所以某个属性查找不到时会沿着它的原型链向上查找

1
2
3
4
5
6
7
8
9
10
11
12
13
function deepCopy(obj) {
if (!isObject(obj)) throw new Error(obj+'不是一个对象!')
let copy = Array.isArray(obj) ? [] : {}
for (let key in obj) {
copy[key] = isObject(obj[key]) ? deepCopy(obj[key]) : obj[key]
}
return copy
}
let me = { name: '张瑞', hobbies: { game:['LOL','吃鸡'], other:[] },like:{} }
let create = Object.create(me)
console.log('create:',create) //如下图
let copy = deepCopy(create)
console.log('copy',copy) //如下图

经过测试,使用Object.create生成出来的create对象,使用for...in方法的拷贝,已经拷贝成功。
因为原形链上的属性也不会被追踪以及复制Object.keys、Reflect.ownKeys、JSON方法本身也不会追踪原型链上的属性,所以使用这些方法并不能拷贝到原型上的属性.

拷贝 Symbol

因为Symbol是一种特殊的数据类型,由于它的特点便是独一无二,所以此时,使用浅拷贝等于深拷贝。
但是此时,如果使用上文中使用for...in的方法来拷贝时,是无法拷贝的,因为现在还没有对Symbol类型做处理。
更多信息可参见阮大的教程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
30
31
32
function deepCopy(obj) {
let cache = []
cache.push(obj);
let _obj = Array.isArray(obj) ? [] : {};
let symbols = Object.getOwnPropertySymbols(obj)
if (symbols.length > 0) {
symbols.forEach(key => {
_obj[key] = isObject(obj[key]) ? deepCopy(obj[key]) : obj[key]
})
}
for (key in obj) {
if(typeof obj[key] === 'object'){
// 使用indexOf可以检测数组中是否存在相同的对象
let index = cache.indexOf(obj[key]);
if (index > -1) {
_obj[key] = cache[index];
}else {
_obj[key] = deepCopy(obj[key])
}
}else {
_obj[key] = obj[key];
}
}
return _obj;
}
let obj1 = {}
let sym = Symbol('Symbollllllll')
obj1[sym] = {
a:1
}
let obj2 = deepCopy(obj1)
console.log(obj2) //{Symbol(Symbollllllll): {a: 1}}

由于Reflect.ownKeys可以直接获取Symbol值,所以Reflect的方法可以直接拷贝。

不可枚举的属性

当拷贝一些描述符属性、getter/setter一类不可枚举的属性时,就需要进一步的处理了。因为上面所写的种种方法,都无法拷贝这些属性(枚举不出来,臣妾做不到啊)。
我们现设置一个不可枚举的对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
let me = { name: '张瑞', hobbies: { game:['LOL','吃鸡'], other:[] },like:{} }

Object.defineProperties(me, {
'hobbies': {
writable: false,
enumerable: false,
configurable: false
},
'name': {
get() {
return '就不告诉你'
},
set(val) {
console.log('不可以,我就叫张瑞')
}
}
})

该实现方法了,恩。。。
不可枚举的属性那可咱办呢。
好吧,还是要看阮大的教程。
对象的扩展-属性的可枚举性和遍历
Object.getOwnPropertyDescriptors
ES5 的Object.getOwnPropertyDescriptor()方法会返回某个对象属性的描述对象(descriptor)。ES2017 引入了Object.getOwnPropertyDescriptors()方法,返回指定对象所有自身属性(非继承属性)的描述对象。

好了开写,这次来个究极版本的

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
function isObject(val) {
return val != null && typeof val === 'object' && Array.isArray(val) === false;
};
function deepCopy(obj, map = new WeakMap()) {
if (!isObject(obj)) return obj
if (map.has(obj)) return map.get(obj)
let _obj = Array.isArray(obj) ? [] : {}
map.set(obj, _obj)
let symbols = Object.getOwnPropertySymbols(obj)
if (symbols.length > 0) {
symbols.forEach(key => {
_obj[key] = isObject(obj[key]) ? deepCopy(obj[key]) : obj[key]
})
}
// 拷贝 描述对象
_obj = Object.create(
Object.getPrototypeOf(_obj),
Object.getOwnPropertyDescriptors(obj)
)
for (let key in obj) {
_obj[key] = isObject(obj[key]) ? deepCopy(obj[key], map) : obj[key];
}

return _obj
}

好啦,快试试。

1
2
3
let myCopy = deepCopy(me)
myCopy.name = '换个名字' // 不可以,我就叫张瑞
console.log(myCopy.name) // 就不告诉你

其实还搜到一个MDN上的方法有点难记。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 下面这个函数会拷贝所有自有属性的属性描述符
function completeAssign(target, ...sources) {
sources.forEach(source => {
let descriptors = Object.keys(source).reduce((descriptors, key) => {
descriptors[key] = Object.getOwnPropertyDescriptor(source, key);
return descriptors;
}, {});

// Object.assign 默认也会拷贝可枚举的Symbols
Object.getOwnPropertySymbols(source).forEach(sym => {
let descriptor = Object.getOwnPropertyDescriptor(source, sym);
if (descriptor.enumerable) {
descriptors[sym] = descriptor;
}
});
Object.defineProperties(target, descriptors);
});
return target;
}

github上的一个方法https://github.com/Tommy-White/deeplyAssign/blob/master/src/index.ts

0%