JavaScript之深浅拷贝

在讲深浅拷贝前,有必要讲下 JavaScript 的内置类型。JavaScript 有 2 种类型,基本类型和引用类型。 基本类型包括 string 、 number 、 boolean 、 null 、 undefined 、 symbo

目录
  1. 类型和堆栈
  2. 赋值、浅拷贝和深拷贝的区别
  3. 浅拷贝
  4. 深拷贝
  5. 手动实现深拷贝
  6. 总结

类型和堆栈#

在讲深浅拷贝前,有必要讲下 JavaScript 的内置类型。JavaScript 有 2 种类型,基本类型和引用类型。

基本类型包括stringnumberbooleannullundefinedsymbol (ES6 新增类型)6 种,以栈的形式存储在内存中。

引用类型主要是 object 类型,包括ObjectArrayDateRegExp等多种类型,以堆的形式存储在内存中。

一般我们声明一个变量,如果变量是基本类型,则变量的值存储在栈内存中。而如果变量是引用类型,变量的值会指向堆内存中引用类型的地址。

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

将某一数值赋给某个变量的过程,称为赋值。基本类型对象赋值是直接在栈内存中创建一个值来。而引用对象的赋值就不一样了,而是将原对象或原数组的引用直接赋给新对象/新数组,新对象/数组只是原对象的一个引用,

当原来的对象或者数组的值改变时,新对象或者新数组也同样跟着改变,同理新对象或者新数字的值改变时,原来的对象或者数组的值也会改变,因为它们的值都来同一个引用,即堆内存中的某个地址的值。

let a = 1;
let b = a;
a = 2;
console.log(b); // 1 赋值后基本对象的值不会随着原来的值的改变而改变
 
const obj1 = { a: 1, b: 3 };
const obj2 = obj1;
obj1.a = 2;
console.log(obj2.a); // 2 赋值后引用类型的值随着原对象的改变而改变
 
obj2.b = 4;
console.log(obj1.b); // 4 同理,原对象值也会随着赋值后对象的值的改变而改变

如果我们在处理引用类型数据到时候,为了防止影响到原来的值,这个时候就不能使用赋值,而是应该使用拷贝。

通过拷贝,在堆内存中重新创建一个新的地址,使得拷贝后变量的值指向新的地址的值,这个地址的值和原来的值是一样的,但是它们是独立的,不会被互相影响。

拷贝有浅拷贝和深拷贝。

  • 浅拷贝:创建一个新的对象和数组,将原对象的各项属性的“值”(数组的所有元素)拷贝过来,是“值”而不是“引用”,只拷贝第一层
  • 深拷贝:创建一个新的对象和数组,将原对象的各项属性的“值”(数组的所有元素)拷贝过来,是“值”而不是“引用”,拷贝所有层

浅拷贝#

浅拷贝只拷贝原对象的第一层,而再深一层的对象或者数组还是原对象的应用。浅拷贝实现非常多,最简单的方法是使用 JS 自带的 API 来可以实现。

对象的浅拷贝

const obj = {
  a: 1,
};
 
const cloneObj = Object.assign({}, obj); // Object.assign API
const cloneObj = { ...obj }; // 解构赋值

数组的浅拷贝

const arr = [1, 2, 3];
 
const cloneArr = Array.from(arr); // Array.from API
const cloneArr = arr.slice(); // 切片
const cloneArr = [...arr]; // 解构赋值

深拷贝#

上面的浅拷贝,只能拷贝了引用类型的第一层的值。如果在更深一层中还存在引用类型,假设这个是为c,则c的值改指向原来的引用类型的值。

const obj = {
  a: 1,
  b: {
    c: 3,
  },
};
 
const cloneObj = Object.assign({}, obj);
 
console.log(cloneObj); // 与 obj 一样
 
obj.a = 10; // 修改第一次的基本类型的值
 
obj.b.c = 33; // 修改更深一层的引用类型的值
 
console.log(cloneObj.a); // 1 基本类型的值不变
 
console.log(cloneObj.b); // { c: 33 } // cloneObj 的值也跟着变了

从上面可以看出,浅拷贝之后的新值如果原来的值修改了更深一层的值后,它的值也会跟着改变,因为浅拷贝只拷贝了第一层,更深一层的值还是原来值的引用,原来的值会影响到浅拷贝的新值。

再看一个示例:

const obj = {
  a: 1,
  b: {
    c: 3,
  },
};
 
const cloneObj = Object.assign({}, obj);
 
obj.b = { d: 4 }; // 修改第一层引用类型的值
 
console.log(cloneObj.b); //  { c: 3 } 不变

为什么这里修改了原对象 b 的值,而浅拷贝的值并没有变化呢?

因为原对象 obj 中 b 的值是引用类型,它指向堆内存中一个地址,它的值为{ c: 3 }的地址。再修改了 b 的值后,相当于把 b 的值指向了堆内存中另一个地址,它的值{d: 4}

通过浅拷贝,堆内存中生产了一个新的地址,它的值也是{ c: 3 },他是独立的,而新的对象 cloneObj 中 b 就是指向这个地址。所以上面代码中,修改了obj.b的值,对cloneObj.b的值一点都没有影响。

但是为什么修改了obj.b.c = 33后,就会对浅拷贝后的对象的值有影响呢?

那是因为浅拷贝的值只拷贝了第一层,虽然它的值指向了一个新的内存地址的值为{ c: 3 }, 但是这个内存地址里面 c 的值3和 原对象 b 下面 c 的值3指向的是同一个内存地址,所有它们都是同一个内存地址值的引用,修改了原对象的值之后,浅拷贝的值也会跟着一起修改。

image.png

如果需要使得拷贝后的对象中 c 的值是不被原来的对象所影响,就需要使用深拷贝了,在堆内存中创建新的值。

使用深拷贝,可以自己封装,也可以直接使用第三方库。比如借助非常有名的第三方工具库lodash的实现深拷贝。

const { cloneDeep } = require("lodash");
 
const obj = {
  a: 1,
  b: {
    c: 3,
  },
};
 
const cloneObj = cloneDeep(obj);

lodashcloneDeep已经非常完美的处理了比如DateRegExp等多种引用类型的值,而且经过了测试,推荐使用。

下面手动实现深拷贝函数。

手动实现深拷贝#

json 转换实现

使用 JSON 的 API 实现深拷贝函数是最简单的,通过 JSON 的 2 次转换即可,代码如下:

function deepClone1(obj) {
  return JSON.parse(JSON.stringify(obj));
}

递归

通过判断拷贝后的值是否是引用类型时,进行递归,直到所有的值都是基本类型为止,这样也可以实现深拷贝函数。

当然,其他还需要处理一下其他的类型比如Array``Date 等类型的值,另外也可以加上缓存,提高性能。

示例代码 1:

function deepClone2(src) {
  let clone = Object.assign({}, src); // 浅拷贝
 
  // 遍历递归
  Object.keys(clone).forEach(
    (key) =>
      (clone[key] =
        typeof src[key] === "object" ? deepClone(src[key]) : src[key])
  );
 
  // 返回值
  return Array.isArray(src) && src.length // 非空数组
    ? (clone.length = src.length) && Array.from(clone) // 设置 length 属性,转换成数组
    : Array.isArray(src) // 空数组
    ? Array.from(src)
    : clone; // 对象
}

示例代码 2:

function deepClone(obj, hash = new WeakMap()) {
  if (obj instanceof RegExp) return new RegExp(obj); // 处理 RegExp 类型的值
  if (obj instanceof Date) return new Date(obj); // 处理 Date 类型的值
 
  if (obj === null || typeof obj !== "object") {
    return obj;
  }
  if (hash.has(obj)) {
    return hash.get(obj);
  }
 
  let t = new obj.constructor();
  hash.set(obj, t);
  for (let key in obj) {
    if (obj.hasOwnProperty(key)) {
      t[key] = deepClone(obj[key], hash);
    }
  }
  return t;
}

总结#

实际上,浅拷贝已经可以解决大部分使用的场景需求了。如果需要用到深拷贝,大多数场景中只需要处理下对象和数组类型即可。如果需要处理复杂引用类型的数据的拷贝,强烈推荐使用lodash库。