
复制一份嵌套数据,用 JSON.parse(JSON.stringify(x)) 还是 structuredClone(x),效果不一样。这件事很多人知道结论,但具体到某种字段到底变成什么样,未必都亲眼看过。下面把 4 种常见字段挨个跑了一遍,环境是 node v26.8.1(V8 内建 structuredClone),每个结果都能用 tools/verify-structuredclone-json.mjs 复现。
4 种字段,一张清单
[undefined]
structuredClone -> Undefined / typeof=undefined
JSON 往返 -> 抛错(stringify(undefined) 返回的是 undefined 不是字符串,parse 读到 undefined 直接 SyntaxError)
[date]
structuredClone -> Date / typeof=object
JSON 往返 -> String / typeof=string
[re]
structuredClone -> RegExp / typeof=object
JSON 往返 -> Object / typeof=object
[map]
structuredClone -> Map / typeof=object
JSON 往返 -> Object / typeof=object
四条里最要命的是第一条。字段的值是 undefined,JSON 往返不是丢值这么简单,是整个调用直接抛错,因为 JSON.stringify(undefined) 返回的不是字符串,是 undefined 本身,JSON.parse 拿到的参数类型就不对。
每条的细节
Date 那条还有个隐藏差异:new Date(0) 往返之后变成字符串 "1970-01-01T00:00:00.000Z"。值能对上,但拿到的是 string,getTime() 调不了了。代码里凡是 .getTime()、.getFullYear() 这类调用,JSON 往返之后一律 TypeError。
RegExp 往返之后对象身份没了。/a/g 的 source 和 flags 还在,但它是普通 Object,test()、exec() 这类方法都不在了。lastIndex 这类状态属性也丢。
Map 变成普通对象,这点最容易在接口层悄悄发生。前端拿到的数据是 Map,一进 JSON 就散成对象,map.get()、map.size 全失效。
还有一个没写进清单的差异:值是函数或者 Symbol 的字段,structuredClone 会直接抛 DataCloneError,把问题摆在台面上;JSON 往返则是静默跳过这个字段,复制出来的对象少一个键,不报任何错。静默的那种更难查,因为下游拿到的是「合法但残缺」的数据,报错点往往在好几层之外。
验证脚本
const cases = {
undefined: undefined,
date: new Date(0),
re: /a/g,
map: new Map([["k", 1]]),
};
for (const [name, v] of Object.entries(cases)) {
const sc = structuredClone(v);
let js = null;
let jsErr = "";
try {
js = JSON.parse(JSON.stringify(v));
} catch (e) {
jsErr = `(往返抛 ${e.name})`;
}
const tag = (obj) => Object.prototype.toString.call(obj).slice(8, -1);
console.log(`[${name}]`);
console.log(` structuredClone -> ${tag(sc)}`);
console.log(` JSON 往返 -> ${jsErr ? "抛错" : tag(js)}${jsErr}`);
}
什么时候用哪个
数据只在页面内存里复制一份,用 structuredClone,类型守得住。数据要发到别处(接口、localStorage、postMessage 走序列化),那本来就要过一遍序列化,JSON 往返丢掉的类型丢了也不冤,但要在边界上想清楚字段是什么类型。
不建议这么干:拿 JSON 往返当「深度复制顺便清洗数据」的技巧用。清洗是把不要的字段去掉,类型是另一件事,两件事搅在一起,哪天丢了个 Date 或者抛了个 SyntaxError,排查起来比当初省下的那几行代码贵得多。