前言
本文将逐步实现一个js深拷贝方法,实现字段描述、Symbol、非枚举字段、原型、循环引用等各种场景值的深拷贝。
封装方法
js
// 拷贝方法
function deepClone(source) {
function loop(source) {
// todo
}
return loop(source)
}
// 验证方法 用于打印和验证结果
function compare(obj1, obj2) {
let consoleText = ''
function loop(obj1, obj2, times = 0, prevKey = '') {
if (times > 4) return
Object.keys(obj1).forEach(key => {
const currentKey = prevKey ? `${prevKey}.${key}` : key
consoleText += `${currentKey} is ${obj1[key] === obj2[key] ? '' : 'not '}equal \n`
console.log(`${currentKey} > `, obj1[key], obj2[key])
if (typeof obj1[key] === "object" || typeof obj2[key] === "object") {
loop(obj1[key], obj2[key], times + 1, currentKey)
}
})
}
loop(obj1, obj2)
console.log(consoleText)
}
基本拷贝
js
const demo = {
string: 'string',
number: 0,
boolean: true,
object: { key: 'key' },
array: [1, 2],
}
实现迭代拷贝字符串,数字,布尔值,基本对象,基本数组:
通过循环key,判断如果是对象,则递归,否则赋值,这里不多做解释,
js
function deepClone(source) {
// 递归方法
function loop(source) {
if (typeof source !== "object") return new Error('source must be an object!')
// 判断复制的目标是数组还是对象
const targetObj = source.constructor === Array ? [] : {}
// 遍历 key 如果依然是对象,则继续迭代,否则赋值
Object.keys(source).forEach(key => {
let newValue
if (typeof source[key] === "object") {
newValue = loop(source[key])
} else {
newValue = source[key]
}
targetObj[key] = newValue
})
return targetObj
}
return loop(source)
}
const cloneDemo = deepClone(demo)
compare(demo, cloneDemo)
日期,正则等类型拷贝
js
const demo = {
date: new Date(),
reg: /^\d+$/,
}
上述方法拷贝以上数据结果却是空对象,与预期不符,所以需要针对这类类型进行特殊处理:
通过判断继承类型,按规则重新初始化数据。
js
function deepClone(source) {
function loop(source) {
。。。
Object.keys(source).forEach(key => {
let newValue
if (typeof source[key] === "object") {
// Date
if (source[key] instanceof Date) newValue = new Date(source[key])
// RegExp
else if (source[key] instanceof RegExp) newValue = new RegExp(source[key])
// todo 按需添加其他判断
else newValue = loop(source[key])
} else {
newValue = source[key]
}
targetObj[key] = newValue
})
return targetObj
}
return loop(source)
}
原型链拷贝
js
class Test { }
class Test1 extends Test {
action() { }
}
const demo = {
test: new Test(),
test1: new Test1(),
}
上述方法拷贝以上数据会丢弃对象原型,与预期不符:
将拷贝值原型指向被拷贝值原型
js
function deepClone(source) {
function loop(source) {
。。。
// 判断复制的目标是数组还是对象
const targetObj = source.constructor === Array ? [] : {}
// 原型指向目标原型
Object.setPrototypeOf(targetObj, Object.getPrototypeOf(source))
。。。
}
return loop(source)
}
SymbolKey,不可枚举键,或包含其它属性描述的键的拷贝
js
const symbolKey = Symbol()
const demo = {
[symbolKey]: 1,
}
Object.defineProperty(demo, "specialKey", {
value: "B",
enumerable: false, // 是否可枚举
configurable: false, // 是否可删除/改变
writable: false, // 是否可赋值
})
上述方法拷贝以上数据会丢弃属性描述,对于不可枚举键更是直接丢弃。与预期不符:
读取属性描述,赋值修改使用值属性描述赋值,遍历方法修改为可遍历SymbolKey的Reflect.ownKeys
js
function deepClone(source) {
// 递归方法
function loop(source) {
...
// 获取原对象属性描述
let sourceDec = Object.getOwnPropertyDescriptors(source)
- Object.keys(source).forEach(key => {
+ Reflect.ownKeys(sourceDec).forEach(key => {
...
- targetObj[key] = newValue
+ Object.defineProperty(targetObj, key, { ...sourceDec[key], value: newValue })
})
return targetObj
}
return loop(source)
}
循环引用对象的拷贝
js
const demo = {}
demo.b = demo
上述方法拷贝以上数据会直接导致死循环。与预期不符:
缓存拷贝过的对象,如果要拷贝的对象是已经拷贝过的,则直接返回上次拷贝的结果
js
function deepClone0(source) {
// 拷贝过的映射
let map = new WeakMap()
function loop(source) {
...
if (map.has(source)) {
return map.get(source)
} else {
// 存储拷贝映射
map.set(source, targetObj)
// 原型指向目标原型
Object.setPrototypeOf(targetObj, Object.getPrototypeOf(source))
...
}
return targetObj
}
return loop(source)
}
完整代码
js
function deepClone0(source) {
// 拷贝过的映射
let map = new WeakMap()
// 递归方法
function loop(source) {
// 判断复制的目标是数组还是对象
const targetObj = source.constructor === Array ? [] : {}
// 如果对象是拷贝过的,直接指向上次拷贝的结果
if (map.has(source)) {
return map.get(source)
} else {
// 存储拷贝映射
map.set(source, targetObj)
// 原型指向目标原型
Object.setPrototypeOf(targetObj, Object.getPrototypeOf(source))
// 获取原对象属性描述
let sourceDec = Object.getOwnPropertyDescriptors(source)
// 遍历目标 Reflect.ownKeys可以遍历Symbol key 及不可枚举 key
Reflect.ownKeys(sourceDec).forEach(key => {
let newValue
if (source[key] && typeof source[key] === "object") {
// Date
if (source[key] instanceof Date) newValue = new Date(source[key])
// RegExp
else if (source[key] instanceof RegExp) newValue = new RegExp(source[key])
// Number
else if (source[key] instanceof Number) newValue = Number(source[key])
// 按需添加其他判断
// other Object 如果值是对象,就递归一下
else newValue = loop(source[key])
} else newValue = source[key]
Object.defineProperty(targetObj, key, {
...sourceDec[key],
value: newValue,
})
})
}
return targetObj
}
return loop(source)
}
实际应用中,大部分上述拷贝场景并不会涉及到,这里仅做说明