JavaScript 中的浅拷贝和深拷贝是什么

83 阅读3分钟

在 JavaScript 复制值时,当复制的是非原始类型 (primitive type) 的数据类型时,例如:对象 (object)、数组 (array) 等,会遇到浅拷贝 (shallow copy) 和深拷贝 (deep copy) 的差异。在面试时被问到这两者的差异,你会怎么回答?如果要你当场手写深拷贝,你会怎么写?假如不确定的话,就一起来看这篇文章吧。

比较浅拷贝 (shallow copy) 和深拷贝 (deep copy)

浅拷贝是指复制值时,满足对象 A 与对象 B 不同,但对象 A 与 对象 B 有相同的属性,并且属性的原型链相同。

而深拷贝则是指在拷贝时,对象 A 与对象 B 不同,两者在原型链上仅是结构相同,但其属性实际的地址不同。在拷贝值时,有可能会遇到变量是多层的场景,例如是一个对象里还有对象,深拷贝的定义会是每一层的值都不会共享地址 (reference)。

这样听起来可能比较抽象,具体来说,以 lodash 这个包提供的效用函数为例,有分成 clonecloneDeep 两种不同效用函数, clone 只用于浅拷贝 (第一层拷贝),但 cloneDeep 可用于深拷贝。下面的例子说明两者的区别:

// lodash 的浅拷贝 clone
var objects = [{ a: 1 }, { b: 2 }];
var shallow = _.clone(objects);
console.log(objects === shallow); // false
console.log(shallow[0] === objects[0]); // true

// lodash 的深拷贝 cloneDeep
var objects = [{ a: 1 }, { b: 2 }];
var deep = _.cloneDeep(objects);
console.log(objects === deep); // false
console.log(deep[0] === objects[0]); // false

在说明完浅拷贝与深拷贝的差别后,面试中常见的接续问题是“手写”浅拷贝与深拷贝。假如你不确定怎么手写这两种拷贝方式,可以继续往下看。

浅拷贝 (shallow copy)

方法一:手动复制值

let objA = {
  a: 1,
  b: { c: 3 },
};

let objB = { a: objA.a, b: objA.b };

console.log(objA === objB); // false
console.log(objA.b === objB.b); // true, 第二层的对象还是指向相同位置

方法二:使用 spread syntax

let objA = {
  a: 1,
  b: { c: 3 },
};

let objB = { ...objA };

console.log(objA === objB); // false
console.log(objA.b === objB.b); // true, 第二层的对象还是指向相同位置

方法三:使用 Object.assign

let objA = {
  a: 1,
  b: { c: 3 },
};

let objB = Object.assign({}, objA);

console.log(objA === objB); // false
console.log(objA.b === objB.b); // true, 第二层的对象还是指向相同位置

深拷贝 (deep copy)

方法一:使用 JSON.parse(JSON.stringify(...))

这个作法是先将对象用 JSON.stringify 序列化为 string,再通过 JSON.parse 转换回对象。要特别注意,这做法只能用于可序列化的对象,有些无法序列化的对象例如:function、HTML 的元素,这些是无法序列化的,所以执行前,需要先确认是否可以序列化,否则在执行 JSON.stringify 时会失败。

let objA = {
  a: 1,
  b: { c: 3 },
};

function deepCopy(item) {
  return JSON.parse(JSON.stringify(item));
}

let objB = deepCopy(objA);

console.log(objA === objB); // false
console.log(objA.b === objB.b); // false

方法二:使用 structuredClone(value)

针对可序列化的对象,有另外一种通过 JavaScript 内置的方法达成深拷贝。这种方法是 structuredClone(value),用法如下。

let objA = {
  a: 1,
  b: { c: 3 },
};

let objB = structuredClone(objA);

console.log(objA === objB); // false
console.log(objA.b === objB.b); // false

方法三:考虑多重情况的递回式深拷贝

通常在面试中,用上述两种方式,可能会被面试官追问说,如果不用这种现成的方法,要如何手写。以下的写法是通过递回的方式,来进行深拷贝。

function deepClone(obj, cache = new WeakMap()) {
  if (cache.has(obj)) {
    return cache.get(obj);
  }

  if (obj === null || typeof obj !== "object" || typeof value === "function") {
    return obj;
  }

  if (obj instanceof Date) return new Date(obj);
  if (obj instanceof RegExp) return new RegExp(obj);

  const result = Array.isArray(obj)
    ? []
    : Object.create(Object.getPrototypeOf(obj));

  cache.set(obj, result);

  for (const key of Reflect.ownKeys(obj)) {
    const value = obj[key];
    result[key] = deepClone(value, cache);
  }

  return result;
}