从0到1实现深拷贝

468 阅读6分钟

什么是深拷贝?什么是浅拷贝?

大家初学js的时候,听到别人在说深拷贝、浅拷贝的时候肯定会觉得很好奇,这到底是个啥?不急,我们通过一个例子来介绍深浅拷贝的区别。

拷贝,顾名思义就是复制一份,我跟你得一样,这就是拷贝。下面我们用ES6的拓展运算符实现一下拷贝。可以看到,arr和arr2的内容是一样的。

let obj = {
  name"jzsp",
  age20,
};

let arr = [123, obj];
let arr2 = [...arr];
console.log(arr); //[ 1, 2, 3, { name: 'jzsp', age: 20 } ]
console.log(arr2); //[ 1, 2, 3, { name: 'jzsp', age: 20 } ]

但是这是一个浅拷贝,什么是浅拷贝呢?深浅拷贝的根本差别是引用类型的数据。我们都知道引用类型是存在堆内存中的,上面的例子中,数组保存了一个引用类型obj,保存的其实是这个引用类型的地址。我们如果修改arr中的obj,看看arr2会有什么变化。

let obj = {
  name"jzsp",
  age20,
};

let arr = [123, obj];
let arr2 = [...arr];
arr[3].name = 'jzsp2号'  
console.log(arr); //[ 1, 2, 3, { name: 'jzsp2号', age: 20 } ]
console.log(arr2); //[ 1, 2, 3, { name: 'jzsp2号', age: 20 } ]

\

可以看到,修改了其中一个引用的属性,那么另一个引用也发生了变化,这是因为他们本质上指向的是同一个对象。大概如下图一样(画的有点joker)

图片.png

深拷贝的含义就是,引用类型我也要单独复制一份,这样的话修改其中一个的引用,另外一个拷贝的对象不会发生变化,大概如下图

图片.png

实现浅拷贝的方法

...

...指的不是无语,是es6的展开运算符。例子就像我上面的那个一样

let arr = [1, 2, 3];
let arr2 = [...arr];    //浅拷贝数组

let obj = {
    name:'jzsp',
    age:20
}

let obj2 = {...obj}   //浅拷贝对象
console.log(obj2)

Object.assign

let arr = [123];
let arr2 = Object.assign([],arr)  //浅拷贝数组
console.log(arr2)   //[1,2,3]


let obj = {
    name:'jzsp',
    age:20
}

let obj2 = Object.assign({},obj)   //浅拷贝对象
console.log(obj2)  //{ name: 'jzsp', age: 20 }

实现深拷贝的方法

JSON

我们可以先通过JSON.stringify()将对象转成JSON字符串,再通过JSON.parse将JSON字符串转成对象。由下图看到,深拷贝确实不会被影响,浅拷贝修改引用类型的值会对源对象有影响。

let obj = {
    name:'jzsp',
    arr:[1,2,3,4]
}

let obj2 = {   //浅拷贝
    ...obj
}

let obj3 = JSON.parse(JSON.stringify(obj))   //深拷贝

obj2.arr[0] = 3
console.log(obj)   
console.log(obj2)
console.log(obj3)

图片.png

但是JSON字符串化是有一定规则的,不符合JSON安全值的内容会被忽略甚至报错(undefined,function,symbol),以及循环引用(对象之间的互相引用,形成一个无限循环)

let obj = {
    name:undefined,
    foo:function(){
        console.log(81)
    },
    [Symbol(123)]:'123'
}
let obj2 = {
   ...obj
}

let obj3 = JSON.parse(JSON.stringify(obj))

console.log(obj)   
console.log(obj2)
console.log(obj3)

可以看到,不安全的JSON值在拷贝的时候都被忽略了。 图片.png

那么接下来,我们需要自己实现一个深拷贝函数。

自己实现深拷贝函数

自己实现的深拷贝主要是需要解决下面几个问题:

  • function被忽略
  • symbol被忽略
  • undefined被忽略
  • 循环引用

1、深拷贝初步实现

let obj = {
    name:undefined,
    foo:function(){
       console.log(81)
    },
    [Symbol(123)]:'123',
    arr:[1,2,3,4]
}

//判断是不是对象类型
function isObject(obj){
    let type = typeof obj
    return obj !== null && (type === "object" || type === "function");
}
function deepClone(val){
    //val是需要拷贝的内容,如果这个需要拷贝的内容不是对象类型,就直接返回即可
    if(!isObject(val)){
        return val
    }

    //是对象类型,那么我们要创建一个新的对象,将内容拷贝到这个对象上并且返回
    let newObj = {}
    for(const key in val){
        //因为我们不知道val对象的key属性是值还是对象,所以我们可以递归的调用方法对这个值做同样的处理
        newObj[key] = deepClone(val[key])  
    }
    return newObj
}

let obj2 = deepClone(obj)
console.log(obj2)
console.log(obj)

图片.png

2、对数组类型进行处理

可以看到,值为数组的属性被拷贝成了对象,所以我们需要对数组进行判断(下面我就只写deepClone中的内容,并且重复注释就去掉拉)

function deepClone(val) {
  if (!isObject(val)) {
    return val;
  }

  //如果值是数组的话,就创建数组,否则就创建对象
  let newObj = Array.isArray(val) ? [] : {};
  for (const key in val) {
    newObj[key] = deepClone(val[key]);
  }

  return newObj;
}

图片.png

3、对函数对象进行处理

函数的内容被拷贝成了一个空对象,这里有个问题:我们需要将函数重新拷贝一份吗?显然是没有必要且做不到的,所以我们判断值是函数类型的时候直接返回即可

function deepClone(val) {
  if (!isObject(val)) {
    return val;
  }
  //如果是函数对象,直接返回这个函数对象
  if(typeof val === 'function'return val
  let newObj = Array.isArray(val) ? [] : {};
  for (const key in val) {
    newObj[key] = deepClone(val[key]);
  }
  return newObj;
}

图片.png

4、对Set和Map进行处理

set和map也是对象,他们也被神奇的拷贝成了空对象,如下。

图片.png 所以我们也要对这两个特殊的对象进行处理。如果判断是set或者map类型的话,就直接新建一个set或者map(注意set和map新建是可以传入可迭代对象的,而set和map本身就是可迭代对象,所以可以直接传入并且new出新的来)

function deepClone(val) {
  if (!isObject(val)) {
    return val;
  }
  if(typeof val === 'function'return val
  //通过instanceof判断类型,并且构造函数是可以传入可迭代对象的
  if(val instanceof Setreturn new Set(val)
  if(val instanceof Mapreturn new Map(val)
  
  let newObj = Array.isArray(val) ? [] : {};
  for (const key in val) {
    newObj[key] = deepClone(val[key]);
  }
  return newObj;
}

图片.png

5、对Symbol进行处理

Symbol有两个问题:

  • 如果Symbol作为对象某个属性的value的话,那么返回的对象和原对象的这个属性的Symbol是同一个,因为它的isObject()false,直接返回了。
  • Symbol作为对象的key的时候不会被拷贝,因为key为Symbol类型的话是不会被for遍历到的

图片.png

function deepClone(val) {

  //如果值是symbol类型,就获取symbol的描述符重新拷贝一份
  //这里有个注意点,需要吧这个isObject判断放在前面,因为symbol不是对象类型,会直接返回的
  if(typeof val === 'symbol'return Symbol(val.description)
  if (!isObject(val)) {
    return val;
  }
  if(typeof val === 'function'return val
  if(val instanceof Setreturn new Set(val)
  if(val instanceof Mapreturn new Map(val)
  let newObj = Array.isArray(val) ? [] : {};
  for (const key in val) {
    newObj[key] = deepClone(val[key]);
  }

  //获取所有的symbol类型的属性,对这些属性值进行深拷贝
  const symbolKeys = Object.getOwnPropertySymbols(val)
  for(const key of symbolKeys){
      newObj[key] = deepClone(val[key])
  }
  return newObj;
}

6、对循环引用进行处理

循环引用主要是会导致递归循环调用,进而导致栈溢出

图片.png

所以我们可以将拷贝的对象保存起来,如果在下次取之前先判断一下之前有没有拷贝过这个对象,有的话就获取这个对象直接返回。这里我们用map来保存

//默认值map是一个空的Map,这样可以实现函数内部值的私有化

function deepClone(val,map = new Map()) {
  if(typeof val === 'symbol'return Symbol(val.description)
  if (!isObject(val)) {
    return val;
  }
  if(typeof val === 'function'return val
  if(val instanceof Setreturn new Set(val)
  if(val instanceof Mapreturn new Map(val)

  //如果前面有保存过拷贝的内容的话,就直接返回
  if(map.has(val)){
      return map.get(val)
  }
  let newObj = Array.isArray(val) ? [] : {};
  //在map中保存一份拷贝的内容
  map.set(val,newObj)
  for (const key in val) {
    newObj[key] = deepClone(val[key],map);
  }
  const symbolKeys = Object.getOwnPropertySymbols(val)
  for(const key of symbolKeys){
      newObj[key] = deepClone(val[key],map)
  }
  return newObj;
}

现在一个自己实现的深拷贝函数就大功告成啦,感谢观看。 图片.png