在Javascript中深拷贝和浅拷贝的使用场景及二者的区别

1、使用场景

  • 概述:深浅拷贝,都是拷贝对象内的数据到新对象,新旧对象内的数据相同。
  • 在js中,当我们需要,使用某个对象内的数据,但是不想修改原本的对象内的数据内容时,通常会使用深浅拷贝来复制数据,并对新的对象内的数据进行操作。例如在完成修改操作时,我们会首先拿到服务器给我们返回的数据,对于这这些后台给我们的数据,我们不应该直接操作,这样会出现一个问题,就是在修改页面时,点了取消按钮,由于Vue的双向绑定的特性,在视图上也会修改数据,所以为了避免这个问题,我们通常可以采用深拷贝或者浅拷贝的方式来解决。下面分别介绍深拷贝和浅拷贝。

2、JS中的数据类型

  • Number
  • String
  • Boolean
  • Null
  • undefined
  • Symbol
  • Object

前面6种类型是简单数据类型,而Object是引用数据类型;简单数据类型没有子类型了,不可以再进行拆分了,而复杂数据类型还有子类型,比如Array,Function,RegExp,Date等对象;正是因为这些子类型的不同导致了深拷贝的各种问题;数据类型的不同,会导致在内存中的存储方式的不同,如果是简单数据类型,存储在栈空间中,存储的是一个值;如果是复杂数据类型,存储在堆空间中,栈空间存储的是一个引用。
正是这种存储方式的差异,导致了浅拷贝和深拷贝的区别。

3、什么是深拷贝和浅拷贝?

  • 浅拷贝是创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本
    类型的值,如果属性是引用类型,拷贝的就是内存地址,所以如果其中一个对象改变了这个地址,就会影响到另一
    个对象。
  • 深拷贝是将一个对象从内存中完整的拷贝一份出来,从堆内存中开辟一个新的区域存放新对象,且修改新对象不会影
    响原对象。

4、赋值和深/浅拷贝的区别?

  • 前提:针对引用数据类型
  • 当我们把一个对象赋值给一个新的变量时,赋的其实是该对象的在栈中的地址,而不是堆中的数据。也就是两个对象指向的是同一个存储空间,无论哪个对象发生改变,其实都是改变的存储空间的内容,因此,两个对象是联动的。
let obj1 = {
  name: '张三',
  arr: [1, [2, 3], 4]
};
let obj2 = obj1;
obj2.name = '张三三';
obj2.arr[2]= [5, 6, 7];
console.log('obj1', obj1); // obj1 {"name":"张三三","arr":[1,[2,3],[5,6,7]]}
console.log('obj2', obj2); // obj2 {"name":"张三三","arr":[1,[2,3],[5,6,7]]}
  • 浅拷贝:重新在堆中创建内存,拷贝前后对象的基本数据类型互不影响,但拷贝前后对象的引用类型因共享同一块内存,会相互影响。
let obj1 = {
   name: '张三',
   arr: [1, [2, 3], 4]
};
let obj2 = shallowClone(obj1);
obj2.name = '张三三';
obj2[1] = [5, 6, 7]; // 新旧对象还是共享一块内存
// 浅拷贝的方法
function shallowClone(source) {
  let target = {};
  for (let i in source) {
    if (source.hasOwnProperty(i)) {
      target[i] = source[i];
    }
  }
  return target;
}

console.log('obj1', obj1); // obj1 {"name":"张三","arr":[1,[5,6,7],4]}
console.log('obj2', obj2); // obj2 {"name":"张三三","arr":[1,[5,6,7],4]}
  • 深拷贝:从堆内存中开辟一个新的区域存放新对象,对对象中的子对象进行递归拷贝,拷贝前后的两个对象互相不影响。
let obj1 = {
      name: '张三',
      arr: [1, [2, 3], 4]
    };
    let obj2 = deepClone(obj1);
    obj2.name = '张三三';
    obj2.arr[1] = [5, 6, 7]; // 新对象跟原型对象不共享内存
    // 深拷贝方法
    function deepClone(obj) {
      if (obj === null) return obj;
      if (obj instanceof Date) return new Date(obj);
      if (obj instanceof RegExp) return new RegExp(obj);
      if (typeof obj !== 'object') return obj;
      let cloneObj = new obj.constructor();
      for (let key in obj) {
        if (obj.hasOwnProperty(key)) { // 不拷贝原型上的对象
          cloneObj[key] = deepClone(obj[key]); // 递归拷贝
        }
      }
      return cloneObj;
    };

    console.log('obj1', obj1) // obj1 {"name":"张三","arr":[1,[2,3],4]}
    console.log('obj2', obj2) // obj2 {"name":"张三三","arr":[1,[5,6,7],4]}

4、JS常见的浅拷贝方式

  • Object.assign
let obj1 = {
  person: {
    name: '刘德华',
    age: 50
  },
  hobby: '唱歌'
}

let obj2 = Object.assign({}, obj1)
obj2.person.name = '科比'
obj2.hobby = '打篮球'
console.log(obj1) //  {"person":{"name":"科比","age":50},"hobby":"唱歌"}
  • 展开运算符
let obj1 = {
  person: {
    name: '刘德华',
    age: 50
  },
  hobby: '唱歌'
}

let obj2 = { ...obj1 }
obj2.person.age = '55'
obj2.hobby = '拍电影'
console.log(obj1) // {"person":{"name":"刘德华","age":"55"},"hobby":"唱歌"}
  • Array.prototype.concat()
let arr = [1, 2, { person: '刘德华' }]
let arr2 = arr.concat()
arr2[2].person = '周杰伦'
console.log(arr) // [1,2,{"person":"周杰伦"}]
  • Array.prototype.slice()
let arr = [1, 2, { person: '刘德华' }]
let arr2 = arr.slice()
arr2[2].person = '周杰伦'
console.log(arr) // [1,2,{"person":"周杰伦"}]

5.JS中运用序列化和反序列化可以完全实现深拷贝吗?

序列化与反序列化

  • 利用JSON.stringify将对象转成JSON字符串,再用JSON.parse把字符串解析成对象
  • 一来一去,新的对象产生了,而且对象会开辟新的栈,实现深拷贝
let arr = [1, 2, { person: '刘德华' }]
let arr2 = JSON.parse(JSON.stringify(arr))
arr2[2].person = '周杰伦'
console.log(arr) // [1,2,{"person":"刘德华"}]
console.log(arr2) // [1,2,{"person":"周杰伦"}]
  • 注意
    • 这种方法虽然可以实现深拷贝,但由于它是依赖于JSON,因此不支持JSON的其他格式
    • JSON只支持object,array,string,number,true,false,null这几种数据或者值,其他的比如函数,undefined,Date,RegExp等数据类型都不支持
    • 对于它不支持的数据都会直接忽略该属性
let arr = [1, 2, { person: '刘德华' }]
let arr2 = JSON.parse(JSON.stringify(arr))
arr2[2].person = '周杰伦'
console.log(arr) // [1,2,{"person":"刘德华"}]
console.log(arr2) // [1,2,{"person":"周杰伦"}]


// 对象中不能有undefined,否则无法序列化
let obj1 = { a: undefined }
let obj2 = JSON.parse(JSON.stringify(obj1))
console.log(obj2); // {}

// 对象中不能有RegExp正则,否则无法序列化
let obj3 = {
  reg: /itwangblog\d/ig
}
let obj4 = JSON.parse(JSON.stringify(obj1))
console.log(obj4) // {}

// Date类型数据会被转换为字符串类型
// 如果对象中存在Date类型的数据,会被转换成字符串,从而丢失Date的一些特性,比如时间格式化等方法
let obj5 = {
  date: new Date()
}
let obj6 = JSON.parse(JSON.stringify(obj5))
console.log(obj5) // {"date":"2022-03-02T08:35:29.743Z"}
console.log(obj6) // {"date":"2022-03-02T08:35:29.743Z"}

6.手写深拷贝参考代码

function deepClone(target, cache = new Map()) { // cache用来防止递归内存溢出
  // 判断:如果有值就直接返回,不需要递归拷贝了
  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;
  }
};
posted @ 2022-03-01 17:39  jscook  阅读(337)  评论(0)    收藏  举报