手写深拷贝

83 阅读5分钟

「这是我参与11月更文挑战的第14天,活动详情查看:2021最后一次更文挑战

1 赋值、浅拷贝和深拷贝的区别

和原数据是否指向同一对象第一层数据为基本数据类型,改变是否会使原数据一同改变原数据中包含子对象,改变是否会使原数据一同改变
赋值
浅拷贝不会
深拷贝不会不会

2 浅拷贝的4种方法

const obj = {
    name: 'AAA',
    age: 18,
    hobbies: {
        h1: 'ball',
        h2: 'chess'
    }
}

// 1.逐个属性赋值
const obj1 = {
    name: obj.name,
    age: obj.age
}
obj1.name = 'BBB'
console.log(obj1);
console.log(obj);

// 2.遍历属性赋值
const obj2 = {}
for (let key in obj) {
    obj2[key] = obj[key]
}
obj2.hobbies.h1 = 'CCC'
console.log(obj2);
console.log(obj);

// 3.Object.assign
const obj3 = Object.assign({}, obj)
obj3.name = 'DDD'
console.log(obj3 === obj);
console.log(obj3);

// 4.展开语法
const obj4 = {...obj}
console.log(obj4 === obj);

3 深拷贝

3.1 JSON序列化

const targetObj = JSON.parse(JSON.stringify(copyObj))

缺点:

  1. 只能拷贝对象中能用JSON表示的数据结构,JSON只支持object,array,string,number,true,false,null等,不支持函数undefinedDateRegExp等;对于不支持的数据会直接忽略该属性

  2. JSON.stringify()只能序列化对象的可枚举的自有属性,即无法拷贝copyObj对象原型链上的属性和方法

  3. 如果对象中存在循环引用,会抛出错误,比如obj.z = obj

3.2 递归克隆

3.2.1 拷贝原始数据类型

function deepClone(target) {
  return target
}

3.2.2 拷贝简单的对象

简单的对象不包括ArrayFunctionDate等子类型

let obj1 = {
  name: "hello",
  child: {
    name: "小明"
  }
}

function deepClone(target) {
  if (target instanceof Object) {
    let dist = {}
    // for in用于遍历对象属性,不建议和数组一起使用
    // 数组可以用foreach 或 for of
    for (let key in target) {
      dist[key] = deepClone(target[key])
    }
    return dist
  } else {
    return target
  }
}

3.2.3 拷贝数组

数组如果用上面的方法拷贝,得到的是对象的形式

所以对deepClone方法进行改进

function deepClone(target) {
  if (target instanceof Object) {
    let dist;
    if (target instanceof Array) {
      dist = [];
    } else {
      dist = {};
    }
    // for in用于遍历对象属性,不建议和数组一起使用
    // 数组可以用foreach 或 for of
    for (let key in target) {
      dist[key] = deepClone(target[key]);
    }
    return dist;
  } else {
    return target;
  }
}

3.2.4 拷贝函数

function deepClone(target){
  if(target instanceof Object){
      let dist ;
      if(target instanceof Array){
        dist = []
      }else if(target instanceof Function){
        dist = function(){
            // 在函数中去执行原来的函数,确保返回的值相同
            return target.call(this, ...arguments);
        }
      }else{
        dist = {};
      }
      for(let key in target){
          dist[key] = deepClone(target[key]);
      }
      return dist;
  }else{
      return target;
  }
}

3.2.5 拷贝正则表达式

const a = /hi\d/ig

一个正则由两部分组成:正则的模式(斜杠之间的内容)以及参数ig

通过正则的source属性能够拿到正则模式,通过正则的flags属性能够拿到正则的参数

function deepClone(target){
  if(target instanceof Object){
      let dist ;
      if(target instanceof Array){
        // 拷贝数组
        dist = [];
      }else if(target instanceof Function){
        // 拷贝函数
        dist = function () {
          return target.call(this, ...arguments);
        };
      }else if(target instanceof RegExp){
        // 拷贝正则表达式
       dist = new RegExp(target.source,target.flags);
      }else{
        // 拷贝普通对象
        dist = {};
      }
      for(let key in target){
          dist[key] = deepClone(target[key]);
      }
      return dist;
  }else{
      return target;
  }
}

3.2.6 拷贝日期

如果日期用上述方法拷贝,返回的是一个字符串

我们需要重新创建日期对象

dist = new Date(target)

3.3 优化deepClone

到目前为止,我们虽然写出了一个可使用的深拷贝函数,但是这个函数仍然存在着许多可优化的地方。(这些优化的地方也是面试官容易问到的地方)。

3.3.1 忽略原型上的属性

for in会遍历包括原型上的所有可迭代属性,深拷贝时一般不应该去遍历原型上的属性,使用for in遍历时最好把原型上的属性和自身属性区分开来,通过hasOwnProperty筛选自身的属性进行遍历

优化后代码:

function deepClone(target){
  if(target instanceof Object){
      let dist ;
      if(target instanceof Array){
        // 拷贝数组
        dist = [];
      }else if(target instanceof Function){
        // 拷贝函数
        dist = function () {
          return target.call(this, ...arguments);
        };
      }else if(target instanceof RegExp){
        // 拷贝正则表达式
       dist = new RegExp(target.source,target.flags);
      }else if(target instanceof Date){
          dist = new Date(target);
      }else{
        // 拷贝普通对象
        dist = {};
      }
      for(let key in target){
          // 过滤掉原型身上的属性
        if (target.hasOwnProperty(key)) {
            dist[key] = deepClone(target[key]);
        }
      }
      return dist;
  }else{
      return target;
  }
}

3.3.2 环状对象的爆栈问题

如果一个对象有属性指向自身,那么会形成一个环,比如:

let a = { name: "小明" }
a.self = a

这样递归调用的过程会无限循环,最终爆栈

所以,我们需要添加递归终止条件:如果一个对象已经被克隆过了就直接使用克隆后的对象,不再进行递归

我们用map来保存可能重复的地址以及它的克隆地址

// 以target为键,以拷贝后的地址为值进行保存
let cache = new Map();
function deepClone(target){
  if(cache.get(target)){
      return cache.get(target)
  }
  if(target instanceof Object){
      let dist ;
      if(target instanceof Array){
        // 拷贝数组
        dist = [];
      }else if(target instanceof Function){
        // 拷贝函数
        dist = function () {
          return target.call(this, ...arguments);
        };
      }else if(target instanceof RegExp){
        // 拷贝正则表达式
       dist = new RegExp(target.source,target.flags);
      }else if(target instanceof Date){
          dist = new Date(target);
      }else{
        // 拷贝普通对象
        dist = {};
      }
      // 将属性和拷贝后的值作为一个map
      cache.set(target, dist);
      for(let key in target){
          // 过滤掉原型身上的属性
        if (target.hasOwnProperty(key)) {
            dist[key] = deepClone(target[key]);
        }
      }
      return dist;
  }else{
      return target;
  }
}

3.3.3 共用缓存导致的互相影响问题

上面我们新增了cache来进行缓存,存在一个问题:每次深拷贝一个对象都会用到这个cache,那么不同拷贝可能会互相影响,示例:

  let a = {
    name:"hello",
  }     
  let a1 = deepClone(a);
  console.log(map);  //{ name: 'hello' } => { name: 'hello' }
  let b = {
    age:24
  }
  let b1 = deepClone(b);
  console.log(map);  //   { name: 'hello' } => { name: 'hello' },{ age: 24 } => { age: 24 } }

解决办法:在调用函数时,每次都创建一个新的map(默认参数),然后如果需要递归,就把这个map往下传。

function deepClone(target,cache = new Map()){
  if(cache.get(target)){
      return cache.get(target)
  }
  if(target instanceof Object){
      let dist ;
      if(target instanceof Array){
        // 拷贝数组
        dist = [];
      }else if(target instanceof Function){
        // 拷贝函数
        dist = function () {
          return target.call(this, ...arguments);
        };
      }else if(target instanceof RegExp){
        // 拷贝正则表达式
       dist = new RegExp(target.source,target.flags);
      }else if(target instanceof Date){
          dist = new Date(target);
      }else{
        // 拷贝普通对象
        dist = {};
      }
      // 将属性和拷贝后的值作为一个map
      cache.set(target, dist);
      for(let key in target){
          // 过滤掉原型身上的属性
        if (target.hasOwnProperty(key)) {
            dist[key] = deepClone(target[key], cache);
        }
      }
      return dist;
  }else{
      return target;
  }
}

3.4 最终函数

function deepClone(target,cache = new Map()){
  if(cache.get(target)){
      return cache.get(target)
  }
  if(target instanceof Object){
      let dist ;
      if(target instanceof Array){
        // 拷贝数组
        dist = [];
      }else if(target instanceof Function){
        // 拷贝函数
        dist = function () {
          return target.call(this, ...arguments);
        };
      }else if(target instanceof RegExp){
        // 拷贝正则表达式
       dist = new RegExp(target.source,target.flags);
      }else if(target instanceof Date){
          dist = new Date(target);
      }else{
        // 拷贝普通对象
        dist = {};
      }
      // 将属性和拷贝后的值作为一个map
      cache.set(target, dist);
      for(let key in target){
          // 过滤掉原型身上的属性
        if (target.hasOwnProperty(key)) {
            dist[key] = deepClone(target[key], cache);
        }
      }
      return dist;
  }else{
      return target;
  }
}