解决循环引用和相同引用的js深拷贝实现

JSON.parse(JSON.stringfy())、MessageChannel这些JavaScript自身的api,想要实现深拷贝存在像不能复制undefined、不能复制函数、不能复制循环引用等问题。

v1

要实现对象深拷贝,第一反应的关键词有:判断类型,递归。代码如下

function isObject(obj) {
  return obj !== null && typeof obj === 'object'
}

function cloneDeep(target) {
  
  if(!isObject(target)) return target

  let result = Array.isArray(target) ? [] : {}
  
  const keys = Object.keys(target);
  for(let i = 0, len = keys.length; i < len; i++) {
    result[keys[i]] = cloneDeep(target[keys[i]])
  }
  return result
}

v2

上面的代码仅能实现基础功能,至于标题中所说的循环引用和相同引用问题并没有处理。
先来解释一下,循环引用是指

var circle = {}
circle.circle = circle
//或者
var a = {}, b = {}
a.b = b
b.a = a

相同引用是指

var arr = [1,2,3]
var obj = {}
obj.arr1 = arr
obj.arr2 = arr

对于循环引用的对象使用v1版本的深拷贝很明显会直接栈溢出。
而对于包含相同对象引用的问题在于,因为复制之前obj.arr1obj.arr2是指向相同对象的,修改其中一个另一个也会改动。使用v1版本的代码拷贝之后,新对象这两个属性的指向将不再相同。
就像这样:

obj.arr1 === obj.arr2 // true
var cloneObj = cloneDeep(obj)
cloneObj.arr1 === cloneObj.arr2 // false

解决方法的思路来自这儿(我是传送门)
1、通过闭包维护一个变量,变量中储存已经遍历过的对象
2、每次递归时判断当前的参数是否已经存在于变量中,如果已经存在,就说明已经递归过该变量,就可以停止这次递归并返回上次递归该变量时的返回值
代码如下

function cloneDeep(obj){ 
  let visitedObjs = [];
  function baseClone(target){

    if(!isObject(target)) return target

    for(let i = 0; i < visitedObjs.length; i++){
      if(visitedObjs[i].target === target){        
        return visitedObjs[i].result;
      }
    }

    let result = Array.isArray(target) ? [] : {} 
    
    visitedObjs.push({ target, result }) 

    const keys = Object.keys(target);
    for(let i = 0, len = keys.length; i < len; i++) {
      result[keys[i]] = baseClone(target[keys[i]])
    }
    return result
  } 
  return baseClone(obj);
}

v2.1

这部分是我理解v2代码时的一些误区,也可以当作是深入理解的过程,可跳过。
学习v2代码的时候,有点没搞懂visitedObjs.push({ target, result })这里push result的含义,因为result是之前调用栈中复制的结果,target就是他要复制的对象,那在

for(let i = 0; i < visitedObjs.length; i++){
  if(visitedObjs[i].target === target){        
    return visitedObjs[i].result;
  }
}

中干嘛还要返回visitedObjs[i].result,直接返回target就完事儿了。

var arr = [1]
var obj = { 1:arr, 2:arr }
var cloneObj = cloneDeep(obj)

为例,调用栈大概如下图所示
解决循环引用和相同引用的js深拷贝实现_第1张图片
其中,第2次是复制obj中第一个arr,第4次是复制第二个,第四次会因为visitedObjs中存在对象的target属性与当前参数target相等,而返回[1]
按照前面所说的想法修改代码,运行结果返回值是{1: [1], 2: [2]}。看起来是正确的,但是在原对象中存在obj[1] === obj[2],在复制之后的对象中这个等式并不成立。
原因是第4次中返回的值visitedObjs[1].result第2次复制的结果,这样使得两个对象地址相同,相等。如果返回target,实际上则是返回了指向原对象中arr数组的引用。也就是说存在obj[1] === cloneObj[2],明显是不正确的。

v3

问题已经解决了,有之前做的LeetCode第一题的经验,可以用map来代替数组进行查找。

function cloneDeep(obj) {
  let vistedMap = new Map();
  function baseClone(target) {
    
    if(!isObject(target)) return target

    if(vistedMap.get(target)) return vistedMap.get(target)

    let result = Array.isArray(target) ? [] : {}

    vistedMap.set(target, result)
    
    const keys = Object.keys(target);
    for(let i = 0, len = keys.length; i < len; i++) {
      result[keys[i]] = baseClone(target[keys[i]])
    }
    return result
  }
  return baseClone(obj)
}

你可能感兴趣的:(javascript)