20250710-放弃_JSON.parse(JSON.stringify())_吧!试试现代深拷贝!

原文摘要

作者:程序员成长指北

原文:mp.weixin.qq.com/s/WuZlo_92q…

最近小组里的小伙伴,暂且叫小A吧,问了一个bug:图片

提示数据循环引用,相信不少小伙伴都遇到过类似问题,于是我问他:

我:你知道问题报错的点在哪儿吗

小A: 知道,就是下面这个代码,但不知道怎么解决。

onst a = {};
const b = { parent: a };
a.child = b; // 形成循环引用

try {
  const clone = JSON.parse(JSON.stringify(a));
} catch (error) {
  console.error('Error:', error.message); // 会报错:Converting circular structure to JSON
}

上面是我将小A的业务代码提炼为简单示例,方便阅读。

  • 这里 a.child 指向 b,而 b.parent 又指回 a,形成了循环引用。
  • 用 JSON.stringify 时会抛出 Converting circular structure to JSON 的错误。

我顺手查了一下小A项目里 JSON.parse(JSON.stringify()) 的使用情况:

图片

一看有50多处都使用了, 使用频率相当高了。

我继续提问:

我:你有找解决方案吗?

小A: 我看网上说可以自己实现一个递归来解决,但是我不太会实现

于是我帮他实现了一版简单的递归深拷贝:

function deepClone(obj, hash = new Map()) {
if (typeof obj !== 'object' || obj === null) return obj;
if (hash.has(obj)) return hash.get(obj);

const clone = Array.isArray(obj) ? [] : {};
  hash.set(obj, clone);

for (const key in obj) {
    if (obj.hasOwnProperty(key)) {
      clone[key] = deepClone(obj[key], hash);
    }
  }
return clone;
}

// 测试
const a = {};
const b = { parent: a };
a.child = b;

const clone = deepClone(a);
console.log(clone.child.parent === clone); // true

此时,为了给他拓展一下,我顺势抛出新问题:

我: 你知道原生Web API 现在已经提供了一个深拷贝 API吗?

小A:???

于是我详细介绍了一下:

主角 structuredClone登场

structuredClone() 是浏览器原生提供的 深拷贝 API,可以完整复制几乎所有常见类型的数据,包括复杂的嵌套对象、数组、Map、Set、Date、正则表达式、甚至是循环引用。

它遵循的标准是:HTML Living Standard - Structured Clone Algorithm(结构化克隆算法)。

语法:

const clone = structuredClone(value);

一行代码,优雅地解决刚才的问题:

const a = {};
const b = { parent: a };
a.child = b; // 形成循环引用

const clone = structuredClone(a);

console.log(clone !== a); // true
console.log(clone.child !== b); // true
console.log(clone.child.parent === clone); // true,循环引用关系被保留

为什么增加 structuredClone

在 structuredClone 出现之前,常用的深拷贝方法有:

方法是否支持函数/循环引用是否支持特殊对象
JSON.parse(JSON.stringify(obj))❌ 不支持函数、循环引用❌ 丢失 DateRegExpMapSet
第三方库 lodash.cloneDeep✅ 支持✅ 支持,但体积大,速度较慢
手写递归✅ 可支持❌ 复杂、易出错

structuredClone 是 原生、极速、支持更多数据类型且无需额外依赖 的现代解决方案。

支持的数据类型

类型支持
Object✔️
Array✔️
Map / Set✔️
Date✔️
RegExp✔️
ArrayBuffer / TypedArray✔️
Blob / File / FileList✔️
ImageData / DOMException / MessagePort✔️
BigInt✔️
Symbol(保持引用)✔️
循环引用✔️

❌ 不支持:

  • 函数(Function)
  • DOM 节点
  • WeakMap、WeakSet

常见使用示例

1. 克隆普通对象

const obj = { a: 1, b: { c: 2 } };
const clone = structuredClone(obj);
console.log(clone);  // { a: 1, b: { c: 2 } }
console.log(clone !== obj); // true

2. 支持循环引用

const obj = { name: 'Tom' };
obj.self = obj;
const clone = structuredClone(obj);
console.log(clone.self === clone);  // true

3. 克隆 Map、Set、Date、RegExp

const complex = {
  mapnew Map([["key""value"]]),
  setnew Set([123]),
  datenew Date(),
  regex/abc/gi
};
const clone = structuredClone(complex);
console.log(clone);

兼容性

提到新的API,肯定得考虑兼容性问题:

图片

  • Chrome 98+
  • Firefox 94+
  • Safari 15+
  • Node.js 17+ (global.structuredClone)

如果需要兼容旧浏览器:

  • 可以降级使用 lodash.cloneDeep
  • 或使用 MessageChannel Hack

很多小伙伴一看到兼容性问题,可能心里就有些犹豫:

"新API虽然好,但旧浏览器怎么办?"

但技术的发展离不开新技术的应用和推广,只有更多人开始尝试并使用,才能让新API真正普及开来,最终成为主流。

建议:

如果你的项目运行在现代浏览器或 Node.js 环境,structuredClone 是目前最推荐的深拷贝方案。 Node.js 17+:可以直接使用 global.structuredClone

原文链接

进一步信息揣测

  • JSON.stringify的隐藏陷阱:处理循环引用时会直接报错,但许多开发者会习惯性使用JSON.parse(JSON.stringify())作为深拷贝的“快捷方式”,直到遇到循环引用问题才意识到其局限性。
  • 递归深拷贝的实战技巧:递归实现时需用Map存储已拷贝对象以解决循环引用问题,但需注意对null、非对象类型的边界处理,以及Array和普通对象的区分。
  • structuredClone API的行业内幕:浏览器原生提供的structuredClone能完美处理循环引用和复杂类型(如Map、Set等),但许多开发者(包括有经验者)可能不知道其存在,仍手动造轮子。
  • 代码审查的隐性价值:通过统计项目中JSON.parse(JSON.stringify())的使用频率(如文中50多处),可快速定位潜在风险点,这种代码审计技巧常被团队内部用于技术债治理。
  • 新手常见误区:遇到问题优先搜索解决方案(如网上递归实现)但缺乏对底层原理的理解(如为什么需要Map),导致无法自主优化或调试。
  • Web API的隐藏能力:类似structuredClone的现代API(如ProxyIntersectionObserver)往往未被充分挖掘,行业内部更依赖社区库(如Lodash的_.cloneDeep),而忽略原生解决方案。
  • 教学中的知识传递策略:资深开发者常通过“提问-引导-扩展”模式(如先问报错点、再给递归方案、最后引入新API)传授经验,这种非文档化的教学方式更高效。