关于深拷贝和浅拷贝的概念和区别在这里就不再赘述了,
而常规的JSON.parse(JSON.stringfy(data)
方式存在很多缺陷,例如无法处理undefined、function、特殊引用类型、循环引用等。
最近尝试手写一下深拷贝的实现,分享一下思路和代码。(完整代码见文章末尾)
深拷贝要考虑的点还是挺复杂的,数据类型太多需要一一处理,具体我是怎么一步步手写以及修改填坑的过程就不多说了,就大概说一下我的代码流程吧。
(定义源数据为target,克隆后的数据为result)
数据类型的判断就通过这个方法即可:
const type = Object.prototype.toString.call(target).match(/\s(\w+)\]/)[1]
返回数据target的类型,首字母大写,之所以返回首字母大写的格式也是为了后面通过new构造函数的方式生成拷贝实例。
先区分基本数据类型和复杂数据类型,基本数据类型直接返回结果:
if (typeof target !== 'object' || target === null) {
// 基本数据类型
result = target
}
针对复杂数据类型再划分成两类,一类是需要递归拷贝的类型:
if (['Array', 'Set', 'Map', 'Object', 'Arguments'].includes(type)) {}
另一类是不需要递归的类型,也就是其他所有类型,逐个做处理赋值。
针对'Array', 'Set', 'Map', 'Object', 'Arguments'
这五中类型需要做递归深度遍历,写递归的核心就是做好变量传参和递归出口,将已处理的结果作为参数递归传入,然后定义递归的出口防止死循环。
只是不同的类型遍历的方式稍有不同:
// Array
target.forEach(v => {
result.push(clone(v))
})
// Set
target.forEach(v => {
result.add(clone(v))
})
// Map
target.forEach((v, k) => {
result.set(k, clone(v))
})
// Object Arguments
Object.keys(target).forEach(k => {
result[k] = clone(target[k])
})
不要使用for…in遍历,因为它会遍历对象的所有可枚举属性,包括原型链上的,原型链上的属性不应该拷贝到新对象自身属性上。
意思就是拷贝赋值时是通过什么方式,比如拷贝一个对象,推荐不使用result = {}
的字面量方式,而是采用new构造函数的方式来初始化result,即result = new Object()
。
这样做的好处就是能保持原始构造函数的原型和继承信息,比如通过es6的class形式创造的Person类,通过对象字面量初始化拷贝时,访问它的构造函数是Object,而通过new初始化拷贝后访问构造函数是Person。
const Constr = target.constructor
result = new Constr()
循环引用即源数据中可能存在内部数据互相引用的问题,如不处理在递归的时候会导致死循环。
处理循环引用可以利用map或weakMap数据结构很巧妙的实现,即每次处理需递归的类型时都把当前要递归的子数据作为key、把result结果作为value写进map里,然后在递归之前先检查一下要递归的数据是否已存在于map中,如果已存在就直接取出value返回。
if (map.get(target)) {
result = map.get(target)
} else {
const Constr = target.constructor
result = new Constr()
map.set(target, result)
// 先给map赋值,在下面写常规的递归逻辑
}
if (type === 'RegExp') {
// RegExp
result = new Constr(target.source, /\w*$/.exec(target))
result.lastIndex = target.lastIndex
}
函数暂时没发现有需要拷贝的场景,再加上函数柯里化的形式难以处理,所以简单点直接赋值返回。
if (type.includes('Function')) {
// Function AsyncFunction GeneratorFunction
result = target
}
if (type === 'Error') {
// Error
result = new Constr(target.message)
result.stack = target.stack
}
例如new Number(1)、Object(Symbol(1))
等,包括未具体判断的类型都统一处理。
else {
try {
// 包装过的 Number String Symbol BigInt
const val = Constr.prototype.valueOf.call(target)
result = Object(val)
} catch (err) {
// other
console.warn(`Uncatched type:${type}`)
console.warn(err)
}
}
可能还有一些没有考虑到的数据类型需要做特殊处理,以后遇到了再更。
/**
* @description: 深克隆方法
* @param {any} target 源数据
* @return {any} 克隆后的数据
*/
function deepClone (target) {
function clone (target, map = new WeakMap()) {
let result
const type = Object.prototype.toString.call(target).match(/\s(\w+)\]/)[1]
if (typeof target !== 'object' || target === null) {
// 基本数据类型
result = target
} else {
if (['Array', 'Set', 'Map', 'Object', 'Arguments'].includes(type)) {
// 可递归遍历的类型处理
// 循环引用处理
if (map.get(target)) {
result = map.get(target)
} else {
const Constr = target.constructor
result = new Constr()
map.set(target, result)
if (type === 'Array') {
// Array
target.forEach(v => {
result.push(clone(v, map))
})
} else if (type === 'Set') {
// Set
target.forEach(v => {
result.add(clone(v, map))
})
} else if (type === 'Map') {
// Map
target.forEach((v, k) => {
result.set(k, clone(v, map))
})
} else {
// Object Arguments
Object.keys(target).forEach(k => {
result[k] = clone(target[k], map)
})
}
}
} else {
// 不可递归遍历的类型处理
const Constr = target.constructor
if (type === 'RegExp') {
// RegExp
result = new Constr(target.source, /\w*$/.exec(target))
result.lastIndex = target.lastIndex
} else if (type.includes('Function')) {
// Function AsyncFunction GeneratorFunction
result = target
} else if (['Date'].includes(type)) {
// Date
result = new Constr(target)
} else if (type === 'Error') {
// Error
result = new Constr(target.message)
result.stack = target.stack
} else if (type === 'URL') {
// URL
result = new Constr(target.href)
} else if (type.includes('Array')) {
// ArrayBuffer TypeArray BigArray ...
result = target.slice()
} else if (type === 'DataView') {
// DataView
result = new Constr(target.buffer.slice(0), target.byteOffset, target.byteLength)
} else {
try {
// 包装过的 Number String Symbol BigInt
const val = Constr.prototype.valueOf.call(target)
result = Object(val)
} catch (err) {
// other
console.warn(`Uncatched type:${type}`)
console.warn(err)
}
}
}
}
return result
}
const res = clone(target)
return res
}
/**
* 测试
*/
const target = {
a: {
b1: 2,
b2: 5,
},
b: [1 , 2, { c1: 3, c2: 4 }],
c: 1,
d: Symbol(123),
e: undefined,
f: null,
g: () => {},
h: new Date(),
i: /123/gi,
j: Object(Symbol(45)),
k: new Error('wrong'),
l: new URL('https://www.baidu.com:80/#/index'),
m: new Number(12),
n: new String('23'),
o: new Boolean('23'),
p: BigInt('23'),
q: Object(BigInt('23')),
r: new ArrayBuffer(10),
s: new Int8Array(10),
}
target.z = { ref: target }
const result = deepClone(target)
console.log(result)
参考链接:https://juejin.cn/post/6844903929705136141