浅拷贝和深拷贝

88 阅读5分钟

一 深、浅拷贝的区别

深、浅拷贝是针对 Object 而言,基本数据类型只有浅拷贝。\

1、层次

  • 浅拷贝 只会将对象的各个属性进行依次复制,并不会进行递归复制,,也就是说只会复制目标对象的第一层属性。
  • 深拷贝 递归拷贝目标对象的所有属性。

2、是否开辟新的栈

  • 浅拷贝 对于目标对象第一层为基本数据类型的数据,直接传值,改变一个对象的值,另一个对象的值不会改变;对于目标对象第一层为引用数据类型的数据,传地址,并且不会开辟新的栈,也就是说,复制的结果是两个对象指向同一个地址,修改其中一个对象的属性,则另一个对象的属性也会改变。
  • 深拷贝 会开辟新的栈,两个对象对应两个不同的地址,修改一个对象的属性,不会改变另一个对象的属性。

二 实现浅拷贝

目标对象属性只有一层时,修改一个对象的值,不会影响另一个对象;目标对象属性有二层及以上时,修改一个对象的值,会影响另一个对象。

  • 1、for...in 和 hasOwnProperty
const checkType = (target)=>{
  //判断是 Object 还是 Array
  return Object.prototype.toString.call(target).slice(8,-1)
}

const shallowClone = (target)=>{
  let result
  if(checkType(target) === 'Object'){
    result = {}
  }else if(checkType(target) === 'Array'){
    result = []
  }
  for (let i in target) {
    //for in 遍历属性,hasOwnProperty 排除不属于自身的属性
    if (target.hasOwnProperty(i)) {
      result[i] = target[i];
    }
  }
  return result
}
  • 2、Obeject.assign
const obj1 = {x:1,y:2}
const obj2 = Object.assign({},obj1)
obj2.x = 3
console.log(obj1)  //{x:1,y:2}
console.log(obj2)  //{x:3,y:2}

const obj3 = {
  x:1,
  y:{
    m:2
  }
}
const obj4 = Object.assign({},obj3)
obj4.x = 3
obj4.y.m = 4
console.log(obj3)  //{x:1,y:{m:4}}
console.log(obj4)  //{x:3,y:{m:4}}
  • 3、Array.concat()
const arr = [1,2,3,4,[5,6]]
const copy = arr.concat()
copy[0] = 5
copy[4][0] = 11
console.log(arr, copy) // [1,2,3,4,[11,6]][5,2,3,4,[11,6]]
  • 4、... 运算符
const arr = [1,2,3,4,[5,6]]
const copy = [...arr]
copy[0] = 5
copy[4][0] = 11
console.log(arr, copy) // [1,2,3,4,[11,6]][5,2,3,4,[11,6]]

三 实现深拷贝

1、用 JSON

const copy = JSON.parse(JSON.stringify(obj))

  • 缺点:
    1. 不支持 Date、正则、undefined、函数、symbol等数据;

    2. 不支持循环引用(即环状结构);

    3. 会抛弃对象的 constructor ,也就是深拷贝之后,不管这个对象原来的构造函数是什么,在深拷贝之后都会变成 Object。

2、用递归

const deepClone = (a)=>{
  if(a instanceof Object){  //不考虑 iframe
    let result
    if(a instanceof Function){  //不能 100% 拷贝
      if (a.prototype) {  //有 prototype 的是普通函数,JS 的函数分为普通函数、箭头函数、异步函数、generator 函数,后两个比较复杂,这里不考虑
        //生成一个新的函数
        // result(...args) 和 a(...args) 的参数、返回值都一样,即 result 和 a 的功能相同,所以是 a 的深拷贝
        result = function (...args) {return a.apply(this, ...args)}
      }else{
        result = (...args)=>{return a.apply(undefined,...args)}
      }
    }else if(a instanceof Array){
      result = []
    }else if(a instanceof Date){
      // a 是日期,日期-0 得到一个数字,new Date(这个数字) 得到一个事件戳
      result = new Date(a - 0)
    }else if(a instanceof RegExp){
      result = new RegExp(a.source,a.flags)
    }else{
      // 其它对象类型都认为是普通对象,在此不作考虑
      result = {}
    }
    for (let key in a) {
      if (a.hasOwnProperty(key)) {
        // a = function(){}
        // a.b = function(){} ,即 函数里面的属性也是函数
        // 如果 result[key] = a[key],传的是地址,修改一个对象的属性会影响另一个对象,必须用递归创造一个新对象
        result[key] = deepClone(a[key])
      }
    }
    return result
  }else{
    //基本数据类型
    return a
  }
}

缺点:没有考虑循环引用 a.self = a,当拷贝 a 时,会拷贝 a.self ,当拷贝 a.self 时,又会拷贝 a ,一直循环,没有出口。
解决方法:每次深拷贝后,将拷贝结果记录下来,如果下次深拷贝时,发现该属性已经拷贝过,就直接 return 拷贝后的结果。

//const cache = new Map()  //cache 如果是全局变量的话,不能清空,每次深拷贝都会有数据残留
const deepClone = (a, cache) => {
  //const cache = new Map()  不能放在函数里面声明,否则每调用一次深拷贝都会创建一个 cache,,最后会创建很多个 cache 而不是一个 cache。
  //既不能放在外面,又不能放在里面,只能放在参数里面
  //第一次深拷贝的时候创建 cache ,之后的递归会将 cache 作为参数传下去,就不会再继续创建 cache 了。
  if (!cache) {
    // key 有可能是对象,所以不能用 new Object(),只能用 Map(),Object 的 key 只能是 string/symbol
    cache = new Map()
  }
  // 拷贝之前先检查下缓存里面是否有 a 的拷贝
  if (cache.get(a)) {
    //如果已经拷贝过 a ,就返回拷贝后的结果
    return cache.get(a)
  }
  //不考虑 iframe(浏览器的元素),如果考虑 iframe ,则这句话不能判断 a 是普通对象
  if (a instanceof Object) {
    let result
    //不能 100% 拷贝函数
    if (a instanceof Function) {
      //有 prototype 的是普通函数,JS 的函数分为普通函数、箭头函数、异步函数、generator 函数,后两个比较复杂,这里不考虑
      if (a.prototype) {
        //生成一个新的函数
        // result(...args) 和 a(...args) 的参数、返回值都一样,即 result 和 a 的功能相同,所以是 a 的深拷贝
        result = function (...args) {return a.apply(this, ...args)}
      } else {
        result = (...args) => {return a.apply(undefined, ...args)}
      }
    } else if (a instanceof Array) {
      result = []
    } else if (a instanceof Date) {
      // a 是日期,日期-0 得到一个数字,new Date(这个数字) 得到一个事件戳
      result = new Date(a - 0)
    } else if (a instanceof RegExp) {
      result = new RegExp(a.source, a.flags)
    } else {
      // 其它对象类型都认为是普通对象,在此不作考虑
      result = {}
    }
    //把拷贝后的结果放在缓存中
    cache.set(a, result)
    for (let key in a) {
      //只拷贝对象自身的属性
      if (a.hasOwnProperty(key)) {
        // a = function(){}
        // a.b = function(){} ,即 函数里面的属性也是函数
        // 如果 result[key] = a[key],传的是地址,修改一个对象的属性会影响另一个对象,必须用递归创造一个新对象
        result[key] = deepClone(a[key], cache)
      }
    }
    return result
  } else {
    //基本数据类型
    return a
  }
}
const a = {
  number: 1, bool: false, str: "hi", empty1: undefined, empty2: null,
  array: [
    {name: "frank", age: 18},
    {name: "jacky", age: 19}
  ],
  date: new Date(2000, 0, 1, 20, 30, 0),
  regex: /.(j|t)sx/i,
  obj: {name: "frank", age: 18},
  f1: (a, b) => a + b,
  f2: function (a, b) { return a + b }
}
//循环引用
a.self = a  

const b = deepClone(a)

console.log(b.self === b) // true
console.log(b.self = "hi")
console.log(a.self !== "hi") //true