深拷贝和浅拷贝

148 阅读4分钟

引言:

在JS中有什么方式可以复制一个对象,它们又有什么样的区别?以及我们该如何去选择复制对象的方法呢? 本文涉及到什么是深浅拷贝,它们与赋值又有什么区别? 深浅拷贝的实现方式有几种?

一、赋值、浅拷贝和深拷贝的概念

  • 1、赋值就把一个对象的引用值赋给内外一个变量。

  • 2、浅拷贝是创建一个新对象,这个对象有着原对象属性值的一份精确拷贝。

    • 如果属性是基本类型,拷贝的就是基本类型的值
    • 如果属性是引用类型,拷贝的就是内存地址 ,所以如果其中一个对象改变了属性值,就会影响到另一个对象
  • 3、深拷贝是将一个对象从内存中完整的拷贝一份出来,从堆内存中开辟一个新的区域存放新对象,包括对象内部为对象的属性值,因此修改新对象不会影响原对象

简单来说就是程度不一样,

  • 赋值:所有的对象的地址值一样
  • 浅拷贝:除最外层对象的地址值不一样,内部的其他对象的地址值一样
  • 深拷贝:所有的对象的地址值都不一样

详情请看下方代码:

let a1 = {
  a: 12,
  b: { c: {} },
};

// 赋值
let a2 = a1;
// 所有的对象的地址值一样
console.log(a1 === a2); // true
console.log(a1.b === a2.b); // true
console.log(a1.b.c === a2.b.c); // true

// 浅拷贝方法,新旧对象还是共享同一块内存
// !!!因为是将对象的内部属性复制到一个空对象中去,所以只有内部的引用值是指向同一地址的
let a3 = Object.assign({}, a1);
// 最外层对象的地址值不一样
console.log(a1 === a3); // false
// 内部的其他对象的地址值一样
console.log(a1.b === a3.b); // true
console.log(a1.b.c === a3.b.c); // true

// 深拷贝方法,新对象跟原对象不共享内存
let a4 = JSON.parse(JSON.stringify(a1));
// 所有的对象的地址值都不一样
console.log(a1 === a4); // false
console.log(a1.b === a4.b); // false
console.log(a1.b.c === a4.b.c); // false

二、浅拷贝的实现方式

1、Object.assign({},源对象)

可以把任意多个的源对象自身的可枚举属性拷贝给目标对象,然后返回目标对象

感兴趣的小伙伴可以去mdn查看详细信息,Object.assign()

2、es6特性:展开运算符 ...

提供了一种简便的方式来浅拷贝一个对象,效果和Object.assign()一样

let obj = {
  a: 12,
  b: { c: {} },
};

let obj2 = { ...obj };
console.log(obj === obj2); // false
console.log(obj.b === obj2.b); // true
console.log(obj.b.c === obj2.b.c); // true

3、拓展:数组中的浅拷贝

  • 1.Array.prototype.concat(),合并数组,只展开第一层
let arr = [1, 2, 3, [4, 5, [7, 8]]];
let arr2 = arr.concat();
console.log(arr === arr2); // false
console.log(arr[3] === arr2[3]); // true
console.log(arr[3][2] === arr2[3][2]); // true
  • 2.Array.prototype.slice(),两个参数,第一参数为开始截取的位置,第二个参数是结束截取的位置
let arr = [1, 2, 3, [4, 5, [7, 8]]];
let arr2 = arr.slice();
console.log(arr === arr2); // false
console.log(arr[3] === arr2[3]); // true
console.log(arr[3][2] === arr2[3][2]); // true

三、深拷贝的实现方式

1、JSON.parse(JSON.stringify())

  • JSON.parse() 将JSON字符串转化为对象
  • JSON.stringify() 将对象转化为JSON字符串
  • 在转换的过程中会产生新的对象,而新对象会开辟新的栈,所以地址值也就不一样了
let obj = {
  a: 12,
  b: { c: {} },
};

let obj2 = JSON.parse(JSON.stringify(obj));
console.log(obj === obj2); // false
console.log(obj.b === obj2.b); // false
console.log(obj.b.c === obj2.b.c); // false

!!! 注意:该方法具有局限性

  • 无法处理正则,正则处理后变成空对象;
  • 无法处理函数,函数会变成null
    • JSON.stringify(function (){}) 会变成undefined,而JSON.parse(undefined)会报错
  • 无法处理undefined,会被忽略;
  • 无法处理Symbol,也会被忽略;
// 正则
console.log(JSON.parse(JSON.stringify(/10086/))); // {}
console.log(JSON.parse(JSON.stringify({ a: /10086/ }))); // { a: {} }

// 函数
console.log(JSON.stringify(function () {})); // undefined
// console.log(JSON.parse(undefined));// 会报错
console.log(JSON.parse(JSON.stringify({ a: function () {} }))); // {}

// undefined
console.log(JSON.parse(JSON.stringify({ a: undefined, b: 1 }))); 
// {b:1} ,值为undefined的a属性没了

// Symbol
console.log(JSON.stringify(Symbol('foo'))); // undefined
console.log(JSON.parse(JSON.stringify({ a: Symbol('foo'), b: 1 }))); 
// {b:1} ,值为Symbol('foo')的a属性没了

2、手写递归实现,使用封装函数递归实现深拷贝

具体实现:

function deepClone(obj) {
  // 参数是数组就创建数组,其他类型就创建对象
  let objClone = Array.isArray(obj) ? [] : {};
  // 如果obj有值,且obj的类型为对象(数组也是对象),基础类型则不执行
  if (obj && typeof obj === 'object') {
    for (key in obj) {
      // 取出每一个对象的中键或者数组中的索引
      if (obj.hasOwnProperty(key)) {
        // 判断这个键是否是对象自身的可遍历的属性
        if (obj[key] && typeof obj[key] === 'object') {
          // 判断属性的值是否为对象,是就递归调用deepClone继续拷贝
          objClone[key] = deepClone(obj[key]);
        } else {
          objClone[key] = obj[key];
        }
      }
    }
  }
  return objClone;
}

let obj = {
  a: 12,
  b: { c: {} },
};

let obj2 = deepClone(obj);
console.log(obj === obj2); // false
console.log(obj.b === obj2.b); // false
console.log(obj.b.c === obj2.b.c); // false