JavaScript—structuredClone()实现深拷贝

56 阅读6分钟

使用 JavaScript 扩展运算符(...)来合并对象,几乎成了每个开发者的肌肉记忆。

const defaults = { theme: 'dark', version: '1.0' };
const userConfig = { theme: 'light', showTips: true };

const finalConfig = { ...defaults, ...userConfig };
// { theme: 'light', version: '1.0', showTips: true }

一行代码,清晰地表达了意图,优雅地完成了合并。

但在某些场景下,它不仅可能带来性能瓶颈,甚至会埋下难以察觉的深坑。

扩展运算符 ... 与 Object.assign

我们先来回顾一下最常用的两种方法:扩展运算符和 Object.assign()

它们的共同点:都是浅拷贝

这是两者最大的共同点,也是它们最大的安全隐患。当对象的属性值是另一个对象或数组时,它们只拷贝引用,而不是创建一个全新的副本。

来看这个例子:

图片

问题大了!  我们只是想修改新对象 merged,却意外地“污染”了原始对象 source。在一个复杂的应用中,这种副作用极难追踪,引发各种诡异的 Bug。

Object.assign() 也存在完全相同的问题

structuredClone()

为了彻底解决浅拷贝带来的数据污染问题,Web 平台引入了一个强大的原生 API:structuredClone():使用结构化克隆算法,创建一个对象的深拷贝。

让我们用 structuredClone() 来重写上面的例子:

const source = {
 user: 'Alice',
 profile: {
    age: 25,
    hobbies: ['coding', 'reading']
  }
};

// 先深拷贝,再合并
const safeMerged = { ...structuredClone(source), user: 'Bob' };

// 再次尝试修改
safeMerged.profile.age = 30;

// 检查原始对象 source
console.log(source.profile.age); // 输出: 25 (🎉 安全!)

当需要处理包含嵌套对象或数组的复杂数据结构时,建议使用 structuredClone() 来保护原始数据。

深拷贝方案的对比

方案支持循环引用支持特殊类型(Date/RegExp/Map)支持函数支持 BigInt/Symbol性能
structuredClone()高(原生实现)
JSON.parse(JSON.stringify())❌(Date 转字符串,RegExp 丢失)❌(Symbol 丢失)
手动递归深拷贝需手动处理需手动适配✅(需特殊处理)✅(需特殊处理)低(纯 JS 递归)
lodash.cloneDeep()中(第三方库)

如何使用structuredClone进行深拷贝?

一、基础用法(90% 场景通用)

1. 克隆普通对象 / 数组

// 克隆普通对象
const originalObj = {
  name: '张三',
  age: 25,
  info: { address: '北京' }, // 嵌套对象
  hobbies: ['读书', '运动']  // 嵌套数组
};
const clonedObj = structuredClone(originalObj);

// 验证深拷贝:修改克隆值不影响原值
clonedObj.info.address = '上海';
clonedObj.hobbies.push('编程');
console.log(originalObj.info.address); // 北京(原值未变)
console.log(originalObj.hobbies);      // ['读书', '运动'](原值未变)

// 克隆数组
const originalArr = [1, [2, 3], { a: 4 }];
const clonedArr = structuredClone(originalArr);
clonedArr[1][0] = 99;
console.log(originalArr[1][0]); // 2(原值未变)

2. 克隆特殊内置类型(Date/RegExp/Map/Set)

structuredClone() 原生支持这些类型,无需额外处理:

// 克隆 Date
const originalDate = new Date('2025-01-01');
const clonedDate = structuredClone(originalDate);
console.log(clonedDate instanceof Date); // true
console.log(clonedDate.getTime() === originalDate.getTime()); // true

// 克隆 RegExp
const originalReg = /abc/gi;
const clonedReg = structuredClone(originalReg);
console.log(clonedReg.source); // abc
console.log(clonedReg.flags);  // gi

// 克隆 Map
const originalMap = new Map([['a', 1], ['b', { c: 2 }]]);
const clonedMap = structuredClone(originalMap);
clonedMap.get('b').c = 99;
console.log(originalMap.get('b').c); // 2(原值未变)

// 克隆 Set
const originalSet = new Set([1, 2, { d: 3 }]);
const clonedSet = structuredClone(originalSet);
clonedSet.forEach(item => {
  if (item.d) item.d = 99;
});
console.log([...originalSet].find(item => item.d)?.d); // 3(原值未变)

3. 处理循环引用(核心优势)

传统 JSON.parse(JSON.stringify()) 会报错,structuredClone() 可正常处理:

// 循环引用对象(自身引用)
const original = { name: '循环引用' };
original.self = original;

// 循环引用数组
const arr = [1, 2];
arr.push(arr);

// 正常克隆
const clonedObj = structuredClone(original);
const clonedArr = structuredClone(arr);

console.log(clonedObj.self === clonedObj); // true(保持循环引用)
console.log(clonedArr[2] === clonedArr);   // true(保持循环引用)

二、进阶用法

1. 转移可转移对象(优化内存)

对于 ArrayBufferMessagePort 等 “可转移对象”,可通过 transfer 选项转移所有权(原对象不可用,克隆对象独占数据,节省内存):

// 创建二进制数据
const buffer = new ArrayBuffer(32);
const view = new Uint8Array(buffer);
view[0] = 42;

// 克隆并转移 buffer 所有权
const cloned = structuredClone(
  { data: view },
  { transfer: [buffer] } // 转移的对象列表
);

console.log(cloned.data[0]); // 42(克隆后可用)
console.log(buffer.byteLength); // 0(原 buffer 已被转移,不可用)

2. 克隆 Error 对象

支持克隆原生 Error 类型(包括自定义属性):

const originalError = new TypeError('参数类型错误');
originalError.code = 400; // 自定义属性
originalError.details = { msg: '请传入数字' };

const clonedError = structuredClone(originalError);
console.log(clonedError.name);    // TypeError
console.log(clonedError.message); // 参数类型错误
console.log(clonedError.code);    // 400(自定义属性保留)
console.log(clonedError.details.msg); // 请传入数字(嵌套属性保留)

三、处理不支持的类型(避坑关键)

structuredClone() 不支持函数、WeakMap/WeakSet、DOM 节点、私有类属性等,需手动适配:

1. 克隆含函数的对象

函数无法被克隆,会抛出 DataCloneError,需先克隆无函数的部分,再手动赋值函数:

const original = {
  name: '张三',
  sayHi: () => console.log('Hi!'), // 函数(不支持克隆)
  info: { age: 25 }
};

// 错误:structuredClone(original) → 抛 DataCloneError

// 正确做法:拆分克隆 + 手动赋值函数
const cloned = structuredClone({
  name: original.name,
  info: original.info // 克隆可序列化的部分
});
cloned.sayHi = original.sayHi; // 手动赋值函数

console.log(cloned.name); // 张三
cloned.sayHi(); // Hi!(函数正常执行)

2. 克隆自定义类实例(保留原型链)

克隆类实例会保留原型链,但私有属性(# 开头)无法克隆,需手动补充:

class Person {
  #privateField = '私有值'; // 私有属性(不支持克隆)
  constructor(name) {
    this.name = name; // 公有属性(支持克隆)
  }
  greet() {
    console.log(`Hi ${this.name}`);
  }
}

const original = new Person('张三');
const cloned = structuredClone(original);

console.log(cloned instanceof Person); // true(原型链保留)
console.log(cloned.name); // 张三(公有属性保留)
// console.log(cloned.#privateField); // 报错:私有属性不可访问

// 手动补充私有属性(若需保留)
cloned.#privateField = original.#privateField;

3. 克隆 WeakMap/WeakSet(降级处理)

WeakMap/WeakSet 不支持克隆,需手动转换为普通 Map/Set 后克隆,再转回:

const originalWeakMap = new WeakMap([[{ id: 1 }, 'value1']]);

// 错误:structuredClone(originalWeakMap) → 抛错

// 降级方案:转为普通 Map 克隆(注意:WeakMap 的键是弱引用,转 Map 会变为强引用)
const tempMap = new Map();
originalWeakMap.forEach((v, k) => tempMap.set(k, v));
const clonedMap = structuredClone(tempMap);

// 转回 WeakMap(按需)
const clonedWeakMap = new WeakMap(clonedMap);

四、兼容性降级

若运行环境不支持 structuredClone()(如低版本浏览器 / Node.js),可降级为 lodash.cloneDeep() 或手动递归深拷贝:

// 兼容函数:优先使用 structuredClone,降级用 lodash
function deepClone(value) {
  if (typeof structuredClone === 'function') {
    try {
      return structuredClone(value);
    } catch (e) {
      // 捕获不支持的类型错误,降级处理
    }
  }
  // 降级:使用 lodash.cloneDeep(需先安装 lodash)
  const _ = require('lodash');
  return _.cloneDeep(value);
}

五、核心注意事项

  1. 深拷贝≠完全一致:克隆后的对象与原值是两个独立对象(引用不同),但值完全相同;
  2. 性能:大数据量 / 复杂结构下,structuredClone() 性能远超手动递归,接近 JSON.parse(JSON.stringify())
  3. 避免滥用:仅需浅拷贝时,优先用扩展运算符({...obj}/[...arr])或 Object.assign(),更高效;
  4. Symbol 克隆:Symbol 作为值会被克隆(生成新的唯一 Symbol),作为对象属性名会保留原 Symbol。

六、总结:使用流程

  1. 基础场景:直接调用 structuredClone(待克隆值)
  2. 含循环引用:无需额外处理,API 自动兼容;
  3. 含不支持类型(函数 / WeakMap 等) :拆分可克隆部分 → 克隆 → 手动补充不支持的部分;
  4. 低版本环境:降级为 lodash.cloneDeep()

structuredClone() 是现代 JS 深拷贝的首选方案,简洁、高效且支持大部分场景,仅需针对少数不支持的类型做适配即可。