js实现深拷贝(深克隆)

关于深拷贝和浅拷贝的概念和区别在这里就不再赘述了,
而常规的JSON.parse(JSON.stringfy(data)方式存在很多缺陷,例如无法处理undefined、function、特殊引用类型、循环引用等。

最近尝试手写一下深拷贝的实现,分享一下思路和代码。(完整代码见文章末尾)

一、整体思路

深拷贝要考虑的点还是挺复杂的,数据类型太多需要一一处理,具体我是怎么一步步手写以及修改填坑的过程就不多说了,就大概说一下我的代码流程吧。
(定义源数据为target,克隆后的数据为result)

1、数据类型划分处理

  • 数据类型的判断就通过这个方法即可:

    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)) {}
    
  • 另一类是不需要递归的类型,也就是其他所有类型,逐个做处理赋值。

2、递归处理

针对'Array', 'Set', 'Map', 'Object', 'Arguments'这五中类型需要做递归深度遍历,写递归的核心就是做好变量传参和递归出口,将已处理的结果作为参数递归传入,然后定义递归的出口防止死循环。
只是不同的类型遍历的方式稍有不同:

  // Arraytarget.forEach(v => {result.push(clone(v))})// Settarget.forEach(v => {result.add(clone(v))})// Maptarget.forEach((v, k) => {result.set(k, clone(v))})// Object ArgumentsObject.keys(target).forEach(k => {result[k] = clone(target[k])})

不要使用for…in遍历,因为它会遍历对象的所有可枚举属性,包括原型链上的,原型链上的属性不应该拷贝到新对象自身属性上。

二、特殊处理

1、拷贝结果的初始化

意思就是拷贝赋值时是通过什么方式,比如拷贝一个对象,推荐不使用result = {}的字面量方式,而是采用new构造函数的方式来初始化result,即result = new Object()
这样做的好处就是能保持原始构造函数的原型和继承信息,比如通过es6的class形式创造的Person类,通过对象字面量初始化拷贝时,访问它的构造函数是Object,而通过new初始化拷贝后访问构造函数是Person。

const Constr = target.constructor
result = new Constr()

2、循环引用的处理

循环引用即源数据中可能存在内部数据互相引用的问题,如不处理在递归的时候会导致死循环。

处理循环引用可以利用map或weakMap数据结构很巧妙的实现,即每次处理需递归的类型时都把当前要递归的子数据作为key、把result结果作为value写进map里,然后在递归之前先检查一下要递归的数据是否已存在于map中,如果已存在就直接取出value返回。

if (map.get(target)) {result = map.get(target)
} else {const Constr = target.constructorresult = new Constr()map.set(target, result)// 先给map赋值,在下面写常规的递归逻辑
}

3、不需要递归的类型

(1)正则
if (type === 'RegExp') {// RegExpresult = new Constr(target.source, /\w*$/.exec(target))result.lastIndex = target.lastIndex
}
(2)函数

函数暂时没发现有需要拷贝的场景,再加上函数柯里化的形式难以处理,所以简单点直接赋值返回。

if (type.includes('Function')) {// Function AsyncFunction GeneratorFunctionresult = target
}
(3)错误对象
if (type === 'Error') {// Errorresult = new Constr(target.message)result.stack = target.stack
}
(4)包装过的基本数据类型

例如new Number(1)、Object(Symbol(1))等,包括未具体判断的类型都统一处理。

else {try {// 包装过的 Number String Symbol BigIntconst val = Constr.prototype.valueOf.call(target)result = Object(val)} catch (err) {// otherconsole.warn(`Uncatched type:${type}`)console.warn(err)}
}

可能还有一些没有考虑到的数据类型需要做特殊处理,以后遇到了再更。

三、完整代码

1、函数实现

/** * @description: 深克隆方法* @param {any} target 源数据* @return {any} 克隆后的数据*/
function deepClone (target) {function clone (target, map = new WeakMap()) {let resultconst 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.constructorresult = new Constr()map.set(target, result)if (type === 'Array') {// Arraytarget.forEach(v => {result.push(clone(v, map))})} else if (type === 'Set') {// Settarget.forEach(v => {result.add(clone(v, map))})} else if (type === 'Map') {// Maptarget.forEach((v, k) => {result.set(k, clone(v, map))})} else {// Object ArgumentsObject.keys(target).forEach(k => {result[k] = clone(target[k], map)})}}} else {// 不可递归遍历的类型处理const Constr = target.constructorif (type === 'RegExp') {// RegExpresult = new Constr(target.source, /\w*$/.exec(target))result.lastIndex = target.lastIndex} else if (type.includes('Function')) {// Function AsyncFunction GeneratorFunctionresult = target} else if (['Date'].includes(type)) {// Dateresult = new Constr(target)} else if (type === 'Error') {// Errorresult = new Constr(target.message)result.stack = target.stack} else if (type === 'URL') {// URLresult = new Constr(target.href)} else if (type.includes('Array')) {// ArrayBuffer TypeArray BigArray ...result = target.slice()} else if (type === 'DataView') {// DataViewresult = new Constr(target.buffer.slice(0), target.byteOffset, target.byteLength)} else {try {// 包装过的 Number String Symbol BigIntconst val = Constr.prototype.valueOf.call(target)result = Object(val)} catch (err) {// otherconsole.warn(`Uncatched type:${type}`)console.warn(err)}}}}return result}const res = clone(target)return res
}

2、测试代码

/*** 测试*/
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


本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!

相关文章

立即
投稿

微信公众账号

微信扫一扫加关注

返回
顶部