跳到主要内容

深拷贝的实现

· 阅读需 10 分钟
熊滔

什么是拷贝?对一个变量进行拷贝是指将其表示的数据复制到另一个变量中,大部分情况下的目的是为了避免直接修改原始数据,以便后续进行多种不同的处理或对比。

最简单的拷贝行为,直接将一个变量赋值给另一个变量

let a = 1;
let b = a;
b = 2;
console.log(a, b); // 1, 2
const originalObj = {
a: 1
}
const copyObj = originalObj;
copyObj.a = 2;
console.log(originalObj.a, copyObj.a); // 2, 2

可以看到,这种直接赋值的方式对于基本数据类型和对象是不一样的,对于基本数据类型(number, string, null, undefined, boolean, symbol, BigInt)直接赋值是完全隔离的,对拷贝的修改不会影响原数据,而对于对象(包括数组、日期、函数等特殊对象)直接进行赋值拷贝,拷贝的仅仅是引用,因此对通过拷贝所持有的引用对数据进行的修改也会影响原来的对象。

因此对于对象的拷贝不能通过直接赋值来达到目的,而是需要将引用的数据进行拷贝,根据拷贝方式的不同,分为浅拷贝和深拷贝。

浅拷贝即只复制对象的一层属性,常见的方式有

const originalObj = {
a: 1
}
const copyObj = {};
for (let key in obj1) {
obj2[key] = obj1[key];
}
const originalObj = {
a: 1
}
const copyObj = {
...obj1
}
const originalObj = {
a: 1
}
const copyObj = Object.assign({}, originalObj);

如果对象的属性的值也是一个对象,那么就会存在问题,复制的仍然是一个引用,对该引用的修改同样会影响原对象

const originalObj = {
a: {
b: 1
}
}
const copyObj = Object.assign({}, originalObj);
copyObj.a.b = 2;
console.log(originalObj.a.b, copyObj.a.b); // 2 2

如果希望对拷贝的值的修改完全不影响原对象,这种情况下就需要进行深拷贝了。

信息

大部分情况下不需要使用深拷贝,原因:

  • 浅拷贝够用了
  • 深拷贝性能较低

JSON.parse、JSON.stringify

一种序列化的方式是将对象先序列为字符串,然后根据字符串重新反序列化为对象,该种方式创建的对象是完全开辟一块新的内存空间,和原来的对象没有任何关系:

const originalObj = {
a: {
b: 1
}
}
const copyObj = JSON.parse(JSON.stringify(originalObj));
copyObj.a.b = 2;

console.log(originalObj.a.b, copyObj.a.b); // 1 2

但是该种方式有很多缺点:

  • 只能处理被序列化的对象,对于特殊类型的对象在反序列化时会丢失特定的类型信息

    • Date 会被序列化为字符串

    • RegExpPromise、Map、Set、WeakMap、WeakSet 会被序列化为空对象

    • Function ,DOM 对象会被忽略

    const originalObj = {
    a: 1,
    b: new Date(),
    c: new RegExp('\\d{2}'),
    d: function () {
    return 'hello'
    },
    e: Promise.resolve('world'),
    f: new Map([['a', 1], ['b', 2]]),
    g: new Set([1, 2, 3]),
    h: new WeakMap(),
    i: new WeakSet()
    }
    const copyObj = JSON.parse(JSON.stringify(originalObj));

  • 无法处理循环引用,如果对象中存在循环引用,则 JSON.stringify 会发生错误

    const originalObj = {}
    originalObj.self = originalObj;
    const copyObj = JSON.parse(JSON.stringify(originalObj));

  • undefinedsymbolBigInt 这些值的无法处理,对于值为

    • undefinedsymbol 的属性在序列化会被忽略

      const originalObj = {
      a: 1,
      b: "Hello",
      c: true,
      d: null,
      e: undefined,
      f: Symbol("symbol"),
      }
      const copyObj = JSON.parse(JSON.stringify(originalObj));

    • BigInt 则会报错

      const originalObj = {
      a: 1,
      b: BigInt(123),
      }
      const copyObj = JSON.parse(JSON.stringify(originalObj));

  • NaNInfinity 会被序列化为 null

    const originalObj = {
    a: 1,
    b: NaN,
    c: Infinity,
    }
    const copyObj = JSON.parse(JSON.stringify(originalObj));

structuredClone 和 postMessage

浏览器提供了一个 structuredClone API,它可以对数据进行深拷贝,它会递归的对输入对象进行拷贝,并且在递归的过程中会维护一个存储访问过的对象的表,防止无限递归和解决循环引用的问题。

该 API 较新,可能存在兼容性问题

另外它只能拷贝可序列化的数据,如果存在不可序列化的数据则会报错,下面这些数据是不可序列化的:

  • Symbol

  • Function

  • Promise

  • WeakMap

  • WeakSet

  • DOM 对象

const originalObj = {
a: 1,
b: "Hello",
c: true,
d: undefined,
e: null,
f: NaN,
g: Infinity,
h: new Date(),
i: /\d{2}/,
j: new Map([['a', 1], ['b', 2]]),
k: new Set([1, 2, 3]),
l: {
a: 1,
}
}
originalObj.self = originalObj;

const copyObj = structuredClone(originalObj);
copyObj.j.set('c', 3);
copyObj.k.add(4);
copyObj.l.a = 2;

也可以使用 MessageChannel 对数据进行拷贝,使用 postMessage 传递数据时,也是深拷贝,其使用的算法和 structuredClone 相同,不同之处在于 MessageChannel 拷贝数据是异步的

const deepCopy = (obj) => {
const channel = new MessageChannel();
return new Promise((resolve) => {
channel.port2.onmessage = (e) => {
resolve(e.data);
}
channel.port1.postMessage(obj);
});
}

手动递归拷贝

除了使用已有的 API,还可以手动递归对象来进行深拷贝。首先实现基本的递归拷贝

const deepCopy = (value) => {
if (typeof value !== 'object' || value === null) {
return value;
}
const newObj = Array.isArray(value) ? [] : {};
for (const key in value) {
if (value.hasOwnProperty(key)) {
newObj[key] = deepCopy(value[key]);
}
}
return newObj;
}
const originalObj = {
a: 1,
b: 'hello',
c: true,
d: undefined,
e: null,
f: NaN,
g: Infinity,
h: {
a: 1,
},
i: [1, 2, 3]
}

const copyObj = deepCopy(originalObj);
copyObj.h.a = 2;
copyObj.i.push(4);

对于基本的对象和数组拷贝没有问题,但是如果存在循环引用,如果自己引用自己,两个对象之间互相引用等,那么就会陷入无限的递归,直至爆栈

const originalObj = {
a: 1,
b: 'hello',
c: true,
d: undefined,
e: null,
f: NaN,
g: Infinity,
h: {
a: 1,
},
i: [1, 2, 3]
}
originalObj.self = originalObj;

const copyObj = deepCopy(originalObj);

为了解决这个问题,需要在递归的过程中维护已处理过的对象,当后续访问到相同对象时,直接返回该对象而不用递归处理

const deepCopy = (value, weakMap = new WeakMap()) => {
if (weakMap.has(value)) return weakMap.get(value);
if (typeof value !== 'object' || value === null) {
return value;
}
const newObj = Array.isArray(value) ? [] : {};
weakMap.set(value, newObj);
for (const key in value) {
if (value.hasOwnProperty(key)) {
newObj[key] = deepCopy(value[key], weakMap);
}
}
return newObj;
}

接下来需要对一些特殊的对象进行处理,如 FunctionDateMapSet 等,对于不能进行深拷贝的对象当作普通对象处理,如 WeakMapWeakSetPromise

const deepCopy = (value, weakMap = new WeakMap()) => {
if (weakMap.has(value)) return weakMap.get(value);
if (typeof value !== 'object' || value === null) {
return value;
}
let newObj;
if (typeof value === 'function') {
newObj = new Function('return ' + value.toString())();
} else if (Array.isArray(value)) {
newObj = [];
} else if (value instanceof Date) {
newObj = new Date(value);
} else if (value instanceof RegExp) {
const { source, flags } = value;
newObj = new RegExp(source, flags);
// lastIndex 不可遍历
newObj.lastIndex = value.lastIndex;
} else if (value instanceof Map) {
newObj = new Map();
for (const [key, val] of value) {
newObj.set(deepCopy(key, weakMap), deepCopy(val, weakMap));
}
} else if (value instanceof Set) {
newObj = new Set();
for (const val of value) {
newObj.add(deepCopy(val, weakMap));
}
} else {
newObj = {};
}
weakMap.set(value, newObj);
for (const key in value) {
if (value.hasOwnProperty(key)) {
newObj[key] = deepCopy(value[key], weakMap);
}
}
return newObj;
}
const originalObj = {
a: 1,
b: 'hello',
c: true,
d: undefined,
e: null,
f: NaN,
g: Infinity,
h: Symbol("symbol"),
i: BigInt(123),
j: {
a: 1,
},
k: [1, 2, 3],
l: new Date(),
m: /\d{2}/,
n: new Map([['a', 1], ['b', 2]]),
o: new Set([1, 2, 3]),
p: new WeakMap(),
q: new WeakSet(),
r: function () {
return 'Hello'
},
s: Promise.resolve("Hello")
}
originalObj.self = originalObj;

const copyObj = deepCopy(originalObj);
copyObj.j.a = 2;
copyObj.k.push(4);
copyObj.n.set('c', 3);
copyObj.o.add(4);
const value = copyObj.r();
console.log(value); // Hello

参考