前言
先划重点,深浅拷贝是一道面试必考题。 在JavaScript中有不同的方法来拷贝一个对象,如果你不是很熟悉的话,在拷贝对象时就容易犯一些错误,那么我们应该如何正确的来拷贝一个对象呢?
本文我将会给大家贯穿深拷贝的问题,由浅入深,坏坏相扣,共4种拷贝方式。
阅读完本篇文章,我们应该明白:
- 什么是深浅拷贝,他们之间有什么区别,和普通赋值又有什么不同?
- 深浅拷贝对象又有哪些方法,他们之间又有什么缺点?
什么是深拷贝,什么是浅拷贝?
深浅拷贝其实就是都是针对的引用数据类型,JS数据类型中分为基本数据类型(值类型) 和引用数据类型;
- 基本数据类型:undefined null Number String Boolean Symbol
- 引用数据类型:Object -> ...Array Set Map WeakMap...
基本数据类型进行复制操作的话是对值进行了拷贝操作,是一个新的值;引用数据类型进行复制操作其实是复制的堆内存中的地址,两个变量是指向同一个地址,改变一个会影响另一个。
// 基本数据类型
var a = 1
var b = a
a = 2
console.log(a, b) // 变量a和b是不同的数据
// 引用数据类型
var obj = { name: '张三' }
var obj2 = obj
obj2.name = '李四'
console.log(obj, obj2) // 变量obj和obj2指向同一个地址 是同一份数据
浅拷贝
既然知道了两个变量指向同一个地址,那么我们怎么切断这个联系呢?我们可以使用浅拷贝来解决这个问题,划重点:浅拷贝只会拷贝引用类型的第一层数据,适合只有一层的数据的时候。
// 浅拷贝 其实就是遍历对象属性
// 方法一
var obj3 = {
info: {
name: '张三'
},
age: 23
}
var obj4 = {}
for(var key in obj3) {
obj4[key] = obj3[key]
}
obj4.age = 24 // 改变obj4的age不会影响obj3 因为浅拷贝只拷贝了第一层数据
obj4.info.sex = '男' // 给obj4的info增加了一个属性sex,导致obj3也增加了
console.log(obj3, obj4)
// 方法二
// Object.assign
// 对象合并,将源对象的所有可枚举值复制到目标对象上,如果源对象里面的值是一个对象,那么只会复制该对象的引用地址
var obj5 = Object.assign({}, obj3)
obj5.age = 24 // 改变obj4的age不会影响obj3 因为浅拷贝只拷贝了第一层数据
obj5.info.sex = '男' // 给obj4的info增加了一个属性sex,导致obj3也增加了
console.log(obj3, obj5)
深拷贝
其实就是浅拷贝和递归的结合版
// 方法一
// 最简单的深拷贝
// JSON.parse(JSON.string)
// 通过先对对象进行JSON字符串化,再通过parse解析出来
// 这个方法有一些弊端
var obj6 = {
info: {
name: '张三'
},
age: 23,
a: undefined,
b: null,
e: function() {},
d: new Set([1, 3, 4]),
f: new Map([{name: '王五'}]),
g: Symbol('name')
}
var obj7 = JSON.parse(JSON.stringify(obj6))
obj7.age = 24
obj7.info.sex = '男'
/**
* 通过打印我们可以看出
* JSON字符串化后再解析出来的数据丢失了一些
* undefined function Symbol丢失了
* Set和Map变成了空对象
* 因为在JSON序列化时会忽略undefined function Symbol,而Map/Set/WeakMap/WeakSet则会被序列化成可枚举的属性
*/
console.log(obj6, obj7)
// 包含循环引用的对象会报错 对象之间相互引用 形成无限循环
var data = {
name: 'foo',
child: null
}
data.child = data
var data2 = JSON.stringify(JSON.parse(data)) // 报错
console.log(data, data2)
// 方法二 ES5
// 遍历+递归
function deepClone(origin, target) {
var target = target || {},
toStr = Object.prototype.toString,
isArr = '[object Array]';
for(var key in origin) {
if(origin.hasOwnProperty(key)) {
if(typeof origin[key] === 'object' && origin[key] !== null) {
target[key] = toStr.call(origin[key]) === isArr ? [] : {}
deepClone(origin[key], target[key])
} else {
target[key] = origin[key]
}
}
}
return target
}
// 方法三
// 这个方法有一个弊端
// 当对象包含循环引用的对象时 形成循环就会报错
function deepClone(origin) {
// undefined 双等于 null
// typeof null = object
if(origin == undefined || typeof origin !== 'object') {
return origin
}
if(origin instanceof Date) {
return new Date(origin)
}
if(origin instanceof RegExp) {
return new RegExp(origin)
}
// constructpr指向构造函数 {} -> Object [] -> Array
let target = new origin.constructor()
for(let key in origin) {
if(origin.hasOwnProperty(key)) {
target[key] = deepClone(origin[key])
}
}
return target
}
var obj8 = deepClone(obj3)
obj8.info.age = 24
console.log(obj8, obj3)
var data = {
name: 'foo',
child: null
}
data.child = data
var data2 = deepClone(data) // 报错 爆栈
console.log(data, data2)
WeakMap
为了解决对象循环引用,导致的爆栈问题,我们需要来认识一个WeakMap;WeakMap对象是一组键/值对的集合,其中的键是弱引用的,且必须是对象类型,值可以是任意的;
WeakMap的键不可枚举,当键所指的对象没有在其他地方引用时,将会被GC垃圾回收机制回收;
Map 是 ES6 中新增的数据结构,Map 类似于对象,但普通对象的 key 必须是字符串或者数字,而 Map 的 key 可以是任何数据类型,其中的键是强引用...
let mapData = { name: '张三' }
let mapData2 = { name: '李四' }
const oBtnWeakMap = new WeakMap()
const oBtnMap = new Map()
oBtnWeakMap.set(mapData, mapData2)
oBtnMap.set(mapData, mapData2)
mapData = null
// mapData赋值为null后,Map里面的mapData2并没有被回收,还被牵着
// 而WeakMap中的因为没有被外界使用,故而被GC垃圾回收机制回收了
浏览器中无法验证变量是否被回收,我们可以在node环境里面来验证下:
从图中可以看出,当给map增加了一个key键的1值的对象后,内存增大,将key赋值为null后,内存并没有得到释放;要想释放内存,需要先delete(key),然后再将key赋值null。
这个时候因为WeakMap时弱引用,当key为null时会自动被GC垃圾回收机制回收,省去了我们手动delete(key)步骤。
利用WeakMap解决深拷贝 对象循环引用爆栈问题
function deepClone(origin, hashMap = new WeakMap()) {
if(origin == undefined || typeof origin !== 'object') {
return origin
}
if(origin instanceof Date) {
return new Date(origin)
}
if(origin instanceof RegExp) {
return new RegExp(origin)
}
const hashKey = hashMap.get(origin)
if(hashKey) {
return hashKey
}
let target = new origin.constructor()
hashMap.set(origin, target)
for(let key in origin) {
if(origin.hasOwnProperty(key)) {
target[key] = deepClone(origin[key], hashMap)
}
}
return target
}
var data = {
name: 'foo',
child: null
}
data.child = data
var data2 = deepClone(data)
console.log(data, data2)