AI 写深拷贝十次有九次是 JSON.parse(JSON.stringify())——Date、Map、循环引用全被吃掉,换 structuredClone 1MB 数据从 45ms 降到 28ms
让 AI 生成一段深拷贝代码,十有八九你拿到的是 JSON.parse(JSON.stringify(obj))。这段代码看起来人畜无害,但它会悄悄吃掉你的 Date、Map、Set 和循环引用——而 2022 年就进 Baseline 的 structuredClone(),在 1MB 复杂对象上比 JSON 往返快约 40%,还一个类型都不丢。
为什么 AI 总爱写 JSON 深拷贝
JSON.parse(JSON.stringify()) 是训练语料里出现频率最高的深拷贝写法,几乎所有 LLM 的默认输出都是它。问题是它根本不是深拷贝,只是「JSON 可序列化子集的拷贝」:
const original = {
createdAt: new Date('2026-09-27T00:00:00Z'),
tags: new Map([['level', 'pro']]),
retries: 3,
};
const jsonClone = JSON.parse(JSON.stringify(original));
console.log(jsonClone.createdAt); // "2026-09-27T00:00:00.000Z" — 字符串,不再是 Date
console.log(jsonClone.tags); // {} — Map 变成空对象
console.log(jsonClone.createdAt instanceof Date); // false
structuredClone() 的同一份输入:
const clone = structuredClone(original);
console.log(clone.createdAt instanceof Date); // true
console.log(clone.tags instanceof Map); // true,且内容完整
console.log(clone.tags.get('level')); // "pro"
实测:1MB 复杂对象,28ms vs 45ms
社区在 Node 22 / Chrome 130 / M1 Max(10K 次迭代均值)上的基准测试结果:
| 方法 | 1KB 扁对象 | 100KB 嵌套 | 1MB 复杂对象 |
|---|---|---|---|
JSON.parse(JSON.stringify()) |
~0.04ms | ~3.2ms | ~45ms |
structuredClone() |
~0.06ms | ~2.1ms | ~28ms |
lodash.cloneDeep() |
~0.15ms | ~5.8ms | ~70ms |
规律是:对象越复杂,structuredClone() 的优势越大(比 JSON 快约 30%~40%,比 lodash cloneDeep 快 2~3 倍)。只有在「扁平的纯原始值对象」上,JSON 路径因为被高度优化才略快一点点,但差距在微秒级。
真正值得换的理由不是速度,是正确性——JSON 往返在字节层面会丢类型,structuredClone 不会。
零拷贝:大 Buffer 的正确姿势
如果你拷贝的对象里有大 ArrayBuffer,拷贝字节本身就很贵。structuredClone 支持 transfer 选项,直接转移所有权而不是复制——这也是 Web Worker postMessage 底层的同一套机制:
const buffer = new ArrayBuffer(64 * 1024 * 1024); // 64MB
// 默认:复制,bytes 翻倍,慢
const copied = structuredClone({ buffer });
// transfer:转移所有权,近乎瞬时,零字节拷贝
const moved = structuredClone({ buffer }, { transfer: [buffer] });
console.log(buffer.byteLength); // 0 — 原 buffer 已被 detach
注意:transfer 后原 buffer 会被「掏空」(detached),后续再读写会抛错。这在把大像素缓冲、音视频数据交给 Worker 时特别有用。
三个 AI 不会告诉你的坑
structuredClone 不是万能深拷贝,遇到以下三类值会直接抛 DataCloneError(同步错误,不是静默失败):
// 1. 函数(哪怕只是对象里的一个方法字段)
structuredClone({ onClick: () => {} }); // ❌ DataCloneError
// 2. DOM 节点
structuredClone(document.body); // ❌ DataCloneError
// 3. 普通 Symbol(Symbol.for 全局符号例外)
structuredClone({ id: Symbol('user') }); // ❌ DataCloneError
两个更隐蔽的行为差异:
// 原型链会断:class 实例变成纯对象
class User { constructor(n){ this.name = n; } greet(){ return this.name; } }
const u = structuredClone(new User('Ana'));
u instanceof User; // false
u.greet; // undefined —— 方法全丢
// 属性描述符会重置:non-enumerable / getter 不再保留
const o = {};
Object.defineProperty(o, 'hidden', { value: 42, enumerable: false });
structuredClone(o).hidden; // 值还在,但变成了可枚举、可写、可配置
所以正确的判断是:纯数据用 structuredClone,带行为/DOM 的用库或定制克隆。
可落地的下一步
- 在项目里全局搜
JSON.parse(JSON.stringify(,逐个确认输入是否是纯 JSON 数据;只要涉及Date/Map/Set/TypedArray/循环引用,就换成structuredClone()。 - 写一个降级包装,避免在极旧环境(Safari < 15.4 / Firefox < 94)直接崩:
const deepClone = (v) =>
typeof structuredClone === 'function'
? structuredClone(v)
: JSON.parse(JSON.stringify(v));
// 更稳:捕获 DataCloneError 再降级
function safeClone(data) {
try {
return structuredClone(data);
} catch (err) {
if (err.name === 'DataCloneError') {
return JSON.parse(JSON.stringify(data)); // 仅限纯数据兜底
}
throw err;
}
}
- 如果对象里有大
ArrayBuffer,记得用{ transfer: [buffer] }走零拷贝路径。顺手把这条规则写进你给 AI 的项目 rules 文件里——比每次重新 review AI 生成的深拷贝代码要省事得多。
评论区
登录后可评论。