JS 深拷贝
1. 深拷贝
1.1 递归深拷贝
1.1.1 从复制地址到复制引用类型
深拷贝与浅拷贝的区别就是: 当遇见引用类型 A 时
- 浅拷贝只会拷贝 A 的地址
- 深拷贝会在堆内存中将 A 复制一遍为 B, 并返回 B 的地址
故而: 深拷贝的第一步就是复制引用类型, 此时需要保证不可枚举属性和 Symbol 属性都能被拷贝
const deepClone = tar => {
// 如果要复制的目标为基础类型或函数的话, 则直接返回对应的值/地址(因为函数还有可能绑定了相应的 lexical environment 和 this 指向)
if (!(typeof tar === 'object' && tar != null)) return tar
const retObj = {}
Reflect.ownKeys(tar).forEach(key => {
// 如果 tar 中存在非函数的引用类型值的话, 则会对该引用值进行递归拷贝, 最终实现深拷贝
retObj[key] = deepClone(tar[key])
})
return retObj
}
1.1.2 支持对数组的深拷贝以及保持原有的原型
// 第一个参数指定原型, 第二个参数则是对 tar 的浅拷贝
const retObj = Object.create(Reflect.getPrototypeOf(tar), Object.getOwnPropertyDescriptors(tar))
// 对于 tar 中的引用类型进行深拷贝
Reflect.ownKeys(tar).forEach(key => {
retObj[key] = deepClone(tar[key])
})
1.1.3 支持 Map, Set, Date, RegExp 的深拷贝
if (tar instanceof Map) {
const retObj = new tar.constructor()
tar.forEach((val, key) => {
retObj.set(key, deepClone(val))
})
} else if (tar instanceof Set) {
const retObj = new tar.constructor()
tar.forEach(val => {
retObj.add(deepClone(val))
})
} else if ([Date, RegExp].includes(tar.constructor)) {
const retObj = new tar.constructor(tar)
} else { // 对象或数组
// 第一个参数指定原型, 第二个参数则是对 tar 的浅拷贝
const retObj = Object.create(Reflect.getPrototypeOf(tar), Object.getOwnPropertyDescriptors(tar))
// 对于 tar 中的引用类型进行深拷贝
Reflect.ownKeys(tar).forEach(key => {
retObj[key] = deepClone(tar[key])
})
}
1.1.4 解决循环引用问题
这里使用 WeakMap 来解决循环引用:
- 每当要拷贝一个对象之前, 先查看一下该对象是否有被记录
- 如果存在记录, 则获取记录, 得到对象的拷贝
- 每当拷贝一个对象之时, 记录下该对象与该对象的拷贝
const deepClone = (tar, map = new WeakMap) => {
if (!(typeof tar === 'object' && tar != null)) return tar
if (map.has(tar)) return map.get(tar)
if (tar instanceof Map) {
const retObj = new tar.constructor()
map.set(tar, retObj) // 在调用 deepClone 之前将 tar-retObj 记录到 map 中, 防止循环引用
tar.forEach((val, key) => {
retObj.set(key, deepClone(val, map))
})
return retObj
} else if (tar instanceof Set) {
const retObj = new tar.constructor()
map.set(tar, retObj)
tar.forEach(val => {
retObj.add(deepClone(val, map))
})
return retObj
} else if (tar instanceof Date || tar instanceof RegExp) {
const retObj = new tar.constructor(tar)
map.set(tar, retObj)
return retObj
} else { // 对象或数组
const retObj = Object.create(Reflect.getPrototypeOf(tar), Object.getOwnPropertyDescriptors(tar))
map.set(tar, retObj)
Reflect.ownKeys(tar).forEach(key => {
retObj[key] = deepClone(tar[key], map)
})
return retObj
}
}
1.1.5 最终版本
const deepClone = (tar, map = new WeakMap) => {
if (!(typeof tar === 'object' && tar != null)) return tar
if (map.has(tar)) return map.get(tar)
if (tar instanceof Map || tar instanceof Set) {
const retObj = new tar.constructor()
map.set(tar, retObj) // 在调用 deepClone 之前将 tar-retObj 记录到 map 中, 防止循环引用
retObj.add ?
tar.forEach(val => retObj.add(deepClone(val, map))) :
tar.forEach((val, key) => retObj.set(key, deepClone(val, map)))
} else if (tar instanceof Date || tar instanceof RegExp) {
const retObj = new tar.constructor(tar)
map.set(tar, retObj)
} else { // 对象或数组
const retObj = Object.create(Reflect.getPrototypeOf(tar), Object.getOwnPropertyDescriptors(tar))
map.set(tar, retObj)
Reflect.ownKeys(tar).forEach(key => {
retObj[key] = deepClone(tar[key], map)
})
}
return retObj
}
1.2 JSON.stringify 和 JSON.parse 的深拷贝
let newObj = JSON.parse(JSON.stringify(obj))
由于 JSON 只支持以下几种数据类型
- Number(只支持
10
进制表示) - String(必须要用
"
包裹) - Boolean(
true
/false
) - Null(
null
) - Object(对象的最后一个键值对后面不能跟
,
) - Array
故而, 当使用 JSON.stringify
和 JSON.parse
进行深拷贝时, 会有以下问题
- 对于以
Function
,Undefined
,Symbol
为值的键值对, key 和 value 都无法拷贝 - 对于以
Date
为值的键值对, 只拷贝其字符串 - 对于以
Map
,Set
,RegExp
为值的键值对, 拷贝时会被转为空对象 - 对于以
BigInt
为值的键值对或者有循环引用的情况时, 拷贝时会报错 - 无法拷贝原型链, 不可枚举属性
2. 深拷贝的替代方案
在使用深拷贝时, 我们通常是想要: 获得一个对象一些属性修改后的状态, 并且原对象的状态不变
那么, 我们是否可以只对将要修改的键值对及其路径进行一定程度的拷贝, 而其他的键值对则直接复制地址即可. 这样的话, 不需要对大量的对象进行复制, 从而可以减小资源的消耗, 提高效率.
如下图所示: 当我们需要修改 A.C.F 时, 对于 A, C 我们只需要深拷贝关键路径的键值对即可(如 A.C, C.F 即可. 而 A.B, A.D, C.E, E.G, E.H 只需要浅拷贝即可)
下面是该想法的简单实现
const change = (tar, {path, value}) => {
const key = path[Symbol.iterator]() // 使用迭代器
const traverse = (tar, prop) => {
if (prop === undefined) return value
const retObj = Object.create(Reflect.getPrototypeOf(tar), Object.getOwnPropertyDescriptors(tar))
retObj[prop] = traverse(tar[prop], key.next().value)
return retObj
}
return traverse(tar, key.next().value)
}
let obj = {b: 1, c: {e: {g: 2, h: 3}, f: 4}, d: 5}
let ret = change(obj, {path: ["c", "f"], value: 233})