使用 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. 转移可转移对象(优化内存)
对于 ArrayBuffer、MessagePort 等 “可转移对象”,可通过 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);
}
五、核心注意事项
- 深拷贝≠完全一致:克隆后的对象与原值是两个独立对象(引用不同),但值完全相同;
- 性能:大数据量 / 复杂结构下,
structuredClone()性能远超手动递归,接近JSON.parse(JSON.stringify()); - 避免滥用:仅需浅拷贝时,优先用扩展运算符(
{...obj}/[...arr])或Object.assign(),更高效; - Symbol 克隆:Symbol 作为值会被克隆(生成新的唯一 Symbol),作为对象属性名会保留原 Symbol。
六、总结:使用流程
- 基础场景:直接调用
structuredClone(待克隆值); - 含循环引用:无需额外处理,API 自动兼容;
- 含不支持类型(函数 / WeakMap 等) :拆分可克隆部分 → 克隆 → 手动补充不支持的部分;
- 低版本环境:降级为
lodash.cloneDeep()。
structuredClone() 是现代 JS 深拷贝的首选方案,简洁、高效且支持大部分场景,仅需针对少数不支持的类型做适配即可。