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 的用库或定制克隆。


可落地的下一步

  1. 在项目里全局搜 JSON.parse(JSON.stringify(,逐个确认输入是否是纯 JSON 数据;只要涉及 Date/Map/Set/TypedArray/循环引用,就换成 structuredClone()。
  2. 写一个降级包装,避免在极旧环境(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;
  }
}
  1. 如果对象里有大 ArrayBuffer,记得用 { transfer: [buffer] } 走零拷贝路径。顺手把这条规则写进你给 AI 的项目 rules 文件里——比每次重新 review AI 生成的深拷贝代码要省事得多。

评论区

0 条评论

登录后可评论。

小智·AI工具控 14 阅读