浅谈js的深拷贝和浅拷贝

61 阅读5分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

js 变量类型

js 的变量有两种类型的值

  1. 基本类型值 存放在栈中的一些简单的数据段 Undefined String Symbol(es6 新增) Null Number Boolean

  2. 引用类型值 引用类型值是引用类型的实例,它是保留在堆内存中的一个对象,引用类型是一种数据结构 最常用的是 Object Array Function 类型,另外还有 Date RegExp Error

浅拷贝

对于浅拷贝的理解: 创建一个新对象,这个对象有着原始对象属性值得一份精确拷贝。如果属性是基本类型,拷贝得就是基本类型得值 如果属性是引用类型,拷贝得就是内存地址,所以如果其中一个对象改变了地址,就会影响到另一个对象!

1 .Object.assign

Object.assign(target,...sources)

ES6 中得拷贝对象方法,接受的第一个参数是拷贝的目标,剩下的参数是拷贝的源对象(可以是多个)

Object.assign(target, ...sources);
let target = {};
let source = { a: { b: 2 } };
Object.assign(target, source);
console.log(target); //{ a: { b: 2 } }

首先通过 Object.assign 将 source 拷贝到 target 中,然后我们尝试将 source 对象中的 b 属性由 2 修改为 10

let target = {};
let source = { a: { b: 2 } };
Object.assign(target, source);
console.log(target); // { a: { b:2 } };
source.a.b = 10;
console.log(source); // { a: { b: 10 } };
console.log(target); // { a: { b: 10 } };

Object.assign 只是在根属性(对象的第一层级)创建了一个新的对象,但是对于属性的值是对象的话只会拷贝一份相同的内存地址 Object.assign 还有一些注意的点是:

  1. 不会拷贝对象继承的属性
  2. 不可枚举的属性
  3. 属性的数据属性/访问器属性
  4. 可以拷贝 Symbol 类型
let obj1 = {
  a: {
    b: 1,
  },
  sym: Symbol(1),
};
Object.defineProperty(obj1, "innumerable", {
  value: "不可枚举属性",
  enumerable: false,
});
let obj2 = {};
Object.assign(obj2, obj1);
obj1.a.b = 2;
console.log("obj1", obj1);
console.log("obj2", obj2);

此时 可以看到 Symbol 类型可以正确拷贝,但是不可枚举的属性被忽略了并且改变了 obj1.a.b 的值,obj2.a.b 的值也会跟着改变,说明依旧存在访问的是堆内存中同一个对象的问题

2. 扩展运算符 利用扩展运算符可以在构造字面量对象时,进行克隆或者属性拷贝

语法:let cloneObj = { ...obj };、

let obj = { a: 1, b: { c: 1 } };
let obj2 = { ...obj };
obj.a = 10;
console.log(obj); //{ a: 10, b: { c: 1 } }
console.log(obj2); //{ a: 1, b: { c: 1 } }
obj.b.c = 2;
console.log(obj); //{ a: 10, b: { c: 2 } }
console.log(obj2); //{ a: 1, b: { c: 2 } }

扩展运算符与 Object.assign()有同样的缺陷,对于值是对象的属性无法完全拷贝成 2 个不同对象,但是如果属性都是基本类型的值的话,使用扩展运算符更加方便

3. Array.prototype.slice

slice()返回一个新的数组对象吗,这一对象是一个由 begin 和 end(不包含 end)决定的原数组的浅拷贝。原始数组不会被改变

语法: arr.slice(begin, end);

在 ES6 以前,没有剩余运算符,Array.from 的时候可以用 Array.prototype.slice 将 arguments 类数组转为真正的数组,它返回一个浅拷贝后的的新数组

Array.prototype.slice.call({ 0: "aaa", length: 1 }); //['aaa]
let arr = [1, 2, 3];
console.log(arr.slice() === arr);

4. Array.prototype.concat

对于数组的 concat 方法其实也是浅拷贝,所以连接一个含有引用类型的数组需要注意修改原数组中的元素的属性会反映到连接后的数组

let arr = [{ a: 1 }, { a: 1 }, { a: 1 }];
let arr2 = [{ b: 1 }, { b: 1 }, { b: 1 }];
let arr3 = arr.concat(arr2);
arr2[0].b = 10;
console.log(arr3); //[ { a: 1 }, { a: 1 }, { a: 1 }, { b: 10 }, { b: 1 }, { b: 1 } ]

深拷贝

浅拷贝只在根属性上在堆内存中创建了一个新的对象,复制了基本类型的值,但是复杂数据类型也就是对象则是拷贝相同的地址,而深拷贝则是对于复杂数据类型在堆内存中开辟了一块内存地址 用于存放复制的对象并且把原有的对象复制过来,这两个对象是相互独立的,也就是两个不同的地址!

将一个对象从内存中完整的拷贝一份出来,从堆内存中开辟一个新的区域存放新对象,且修改新对象不会影响原对象

1. 一个简单的深拷贝

let obj1 = {
  a: {
    b: 1,
  },
  c: 1,
};

let obj2 = {};

obj2.a = {};
obj2.c = obj1.c;
obj2.a.b = obj1.a.b;

console.log("原obj1:", obj1); //原obj1: { a: { b: 1 }, c: 1 }
console.log("复制的obj2", obj2); //复制的obj2 { a: { b: 1 }, c: 1 }
// 修改原obj1中的b为2
obj1.a.b = 2;
//观察复制的obj2会不会发生改变
console.log("修改后的obj1:", obj1); // 修改后的obj1: { a: { b: 2 }, c: 1 }
console.log("obj2:", obj2); // obj2: { a: { b: 1 }, c: 1 }

在上面代码中,我们新建了一个 obj2 对象,同时根据 obj1 对象的 a 属性是一个引用类型,我们给 obj2.a 的值也新建了一个新对象(即在内存中新开辟了一块内存地址) 然后把 obj1.a.b 属性的值数字 1 复制给 obj2.a.b。因为数字 1 是基本类型的值,所以改变 obj.a.b 的值后,obj2.a 不会受到影响,因为他们的引用是完全 2 个独立的对象,这就完成了一个简单的深拷贝

2. JSON.stringify

JSON.stringify()是目前前端开发过程中最常用的深拷贝方式,原理是把有个对象序列化成为一个 JSON 字符串,将对象的内容转换成字符串的形式再保存到磁盘上,再用 JSON.parse()反序列化将 JSON 字符串变成一个新的对象

let obj1 = {
  a: 1,
  b: [1, 2, 3],
};

let str = JSON.stringify(obj1);
let obj2 = JSON.parse(str);
console.log(obj2); //{ a: 1, b: [ 1, 2, 3 ] }

//修改obj1
obj1.a = 2;
obj1.b.push(4);
console.log(obj1); //{ a: 2, b: [ 1, 2, 3, 4 ] }
console.log(obj2); //{ a: 1, b: [ 1, 2, 3 ] }

通过 JSON.stringify 实现深拷贝有几点要注意

  1. 拷贝的对象的值中如果有函数,undefined,symbol 则经过 JSON.stringify()序列化后的 JSON 字符串中这个键值对会消失
  2. 无法拷贝不可枚举的属性,无法拷贝对象的原型链
  3. 拷贝 Date 引用类型会变成字符串
  4. 拷贝 RegExp 引用类型会变成空对象
  5. 对象中含有 NaN、Infinity 和-Infinity,则序列化的结果会变成 null
  6. 无法拷贝对象的循环应用(即 obj[key] = obj)

3. 自己实现一个深拷贝

function deepClone(source) {
  const targetObj = source.constructor === Array ? [] : {}; // 判断复制的目标是数组还是对象
  for (let keys in source) {
    // 遍历目标
    if (source.hasOwnProperty(keys)) {
      if (source[keys] && typeof source[keys] === "object") {
        // 如果值是对象,就递归一下
        targetObj[keys] = source[keys].constructor === Array ? [] : {};
        targetObj[keys] = deepClone(source[keys]);
      } else {
        // 如果不是,就直接赋值
        targetObj[keys] = source[keys];
      }
    }
  }
  return targetObj;
}

var str1 = {
  arr: [1, 2, 3],
  obj: {
    key: "value",
  },
  fn: function () {
    return 1;
  },
};
var str3 = deepClone(str1);

console.log(str3 === str1); // false
console.log(str3.obj === str1.obj); // false
console.log(str3.fn === str1.fn); // true