ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

【JS 对象】深拷贝还在手写递归?StructuredClone API 了解一下!

【JS 对象】深拷贝还在手写递归?StructuredClone API 了解一下! 1. 深拷贝踩坑现场JSON.parse 和手写递归到底差在哪先聊一个我上周真实遇到的 bug。一个配置面板组件用户改完表单后我做了const draft { ...formData }结果用户点取消原表单数据居然跟着变了。排查半小时才发现formData里嵌套了一层rules数组展开语法只复制了第一层draft.rules和formData.rules指向同一块内存。这就是典型的浅拷贝陷阱。JS 里对象和数组这类引用类型变量存的不是数据本身而是指向堆内存的地址。赋值、展开语法...、Object.assign()都只复制这个地址所以改副本等于改原件。要彻底断开这层引用就得用深拷贝。深拷贝的常见方案有三代。第一代是JSON.parse(JSON.stringify(obj))一行搞定但坑极多Date会变成字符串Map/Set/RegExp会变成空对象{}undefined和函数直接消失遇到循环引用直接抛TypeError: Converting circular structure to JSON。它只适合纯 JSON-safe 的数据稍微复杂一点就翻车。第二代是手写递归。核心思路是判断类型原始类型直接返回对象和数组新建容器后逐层递归。但一个健壮的递归函数要处理Date、RegExp、Map、Set、ArrayBuffer、循环引用用WeakMap记录已拷贝对象打断循环、原型链……写全了轻松上百行还容易漏边界情况。Lodash 的_.cloneDeep就是这么堆出来的。第三代就是浏览器原生的structuredClone()。它背后是结构化克隆算法早就用在postMessage、IndexedDB里了现在直接暴露成全局函数。一行structuredClone(obj)就能处理Date、Map、Set、RegExp、ArrayBuffer、TypedArray还原生支持循环引用性能是 C 级别。它不能克隆函数、DOM 节点也不保留原型链但这恰恰是它的设计定位——复制纯数据状态不复制行为。这篇文章我会把三代方案的差异用可运行的代码摆出来给你一份能直接复制的深拷贝工具函数和兼容性检测代码最后演示怎么用 TaoToken 统一 Key 通道接入 AI 辅助代码审查让模型帮你验证深拷贝结果是否符合预期。适合正在被深拷贝坑过、想搞清楚该用哪个方案的前端同学。2. TaoToken 前置准备统一 Key 与 API 通道接入 AI 代码审查在写深拷贝工具函数的过程中我经常需要让 AI 帮我 review 边界情况比如「这个递归函数遇到WeakMap会不会漏」「循环引用检测逻辑对不对」。如果每次都要切换不同模型的 Key、改 Base URL效率很低。TaoToken 的作用就是把多个模型的调用收敛到一个 Key、一个 API 通道上前端项目里做 AI 辅助审查时特别省事。先说清楚它是什么TaoToken 是一个统一的模型 API 接入层你拿一个 Key 就能调用不同厂商的模型Base URL 固定不用为每个模型单独配环境变量。对前端开发者来说这意味着你可以在 Node 脚本、Vite 插件、甚至浏览器端注意别暴露 Key里用同一套配置做代码审查。接入分三步。第一步去官网注册并拿到 API Key。官网地址是 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 注册后在控制台创建 Key。控制台入口https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewrite 。Key 管理页面在 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite 建议给每个项目单独建一个 Key方便排查和吊销。第二步确认 Base URL。API 根地址是 https://taotoken.net/api 注意这个地址不带 UTM 参数直接用于代码里的baseURL配置。所有模型共用这一个入口模型 ID 通过请求体里的model字段区分。第三步选模型。如果你只是做代码审查、问答这类轻量任务用模型对话页面先试效果https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_contentmodel-chatutm_campaignrewrite 。如果你要长期跑编码 Agent、批量审查建议看 Coding Planhttps://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite 额度更划算。接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 里面有各语言的调用示例。这里要提醒一句Key 是敏感信息前端项目里绝对不要硬编码到会被打包进浏览器的代码里。做代码审查脚本时把 Key 放在 Node 侧的.env文件通过process.env读取。下面章节我会给出完整的配置片段。3. 可复制配置深拷贝工具函数 TaoToken 接入片段这一节给你两份可直接复制的代码。第一份是深拷贝工具函数包含兼容性检测和降级策略第二份是 TaoToken 的接入配置用 Node 脚本调模型审查深拷贝代码。先看深拷贝工具函数。核心思路是优先用structuredClone不支持时降级到递归实现递归实现里用WeakMap处理循环引用// deepClone.js export function supportsStructuredClone() { if (typeof structuredClone ! function) return false; try { const test { date: new Date(), map: new Map([[k, 1]]), set: new Set([1]) }; test.self test; const cloned structuredClone(test); return cloned.date instanceof Date cloned.map instanceof Map cloned.set instanceof Set cloned.self cloned; } catch (e) { return false; } } const HAS_NATIVE supportsStructuredClone(); function fallbackClone(obj, seen new WeakMap()) { if (obj null || typeof obj ! object) return obj; if (seen.has(obj)) return seen.get(obj); if (obj instanceof Date) return new Date(obj.getTime()); if (obj instanceof RegExp) return new RegExp(obj.source, obj.flags); if (obj instanceof Map) { const m new Map(); seen.set(obj, m); obj.forEach((v, k) m.set(fallbackClone(k, seen), fallbackClone(v, seen))); return m; } if (obj instanceof Set) { const s new Set(); seen.set(obj, s); obj.forEach(v s.add(fallbackClone(v, seen))); return s; } if (ArrayBuffer.isView(obj)) { return new obj.constructor(obj); } const clone Array.isArray(obj) ? [] : {}; seen.set(obj, clone); for (const key of Reflect.ownKeys(obj)) { clone[key] fallbackClone(obj[key], seen); } return clone; } export function deepClone(obj) { if (HAS_NATIVE) { try { return structuredClone(obj); } catch (e) { // 遇到函数、DOM 节点等不可克隆类型时降级 return fallbackClone(obj); } } return fallbackClone(obj); }这份代码的关键点supportsStructuredClone不只是判断函数存在还实际跑一遍Date、Map、Set、循环引用确保环境真的支持。降级函数用WeakMap记录已拷贝对象遇到循环引用直接返回缓存避免栈溢出。Reflect.ownKeys能拿到 Symbol 键比for...in更完整。再看 TaoToken 接入配置。在项目根目录建.env# .env TAOTOKEN_API_KEYsk-你的Key TAOTOKEN_BASE_URLhttps://taotoken.net/api TAOTOKEN_MODELclaude-sonnet-4-20250514然后写一个审查脚本review.mjs把深拷贝代码发给模型检查// review.mjs import fs from node:fs; import dotenv/config; const code fs.readFileSync(./deepClone.js, utf8); const res await fetch(${process.env.TAOTOKEN_BASE_URL}/v1/chat/completions, { method: POST, headers: { Content-Type: application/json, Authorization: Bearer ${process.env.TAOTOKEN_API_KEY} }, body: JSON.stringify({ model: process.env.TAOTOKEN_MODEL, messages: [ { role: system, content: 你是资深前端代码审查员专注深拷贝边界情况。 }, { role: user, content: 请审查以下深拷贝实现指出循环引用、类型丢失、原型链问题\n\n${code} } ], temperature: 0.2 }) }); const data await res.json(); console.log(data.choices[0].message.content);如果你用的是 Claude Code 这类编码 Agent配置方式略有不同。Claude Code 走 Anthropic 兼容协议需要设置环境变量指向 TaoToken 的 Anthropic 端点具体参考 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentclaudecodeutm_campaignrewrite 。核心三件套是Base URL 填https://taotoken.net/apiKey 填你的TAOTOKEN_API_KEYModel ID 填你选的模型比如claude-sonnet-4-20250514。这三项在 Cline、CC Switch、Codex 的auth.json里配置逻辑一致都是替换 Base URL、Key、Model ID。4. 验证请求与成功结果跑通深拷贝测试和 AI 审查配置写完了得验证真的能跑通。先验证深拷贝工具函数写一个测试脚本覆盖Date、Map、Set、循环引用、ArrayBuffer这些场景// test.mjs import { deepClone, supportsStructuredClone } from ./deepClone.js; console.log(原生 structuredClone 可用:, supportsStructuredClone()); const original { name: config, createdAt: new Date(2024-01-01), tags: new Set([a, b]), meta: new Map([[level, 99]]), buffer: new Int8Array([1, 2, 3]).buffer, nested: { deep: { value: 1 } } }; original.self original; const cloned deepClone(original); console.log(Date 类型保持:, cloned.createdAt instanceof Date); console.log(Set 类型保持:, cloned.tags instanceof Set); console.log(Map 类型保持:, cloned.meta instanceof Map); console.log(循环引用指向自身:, cloned.self cloned); console.log(循环引用不指向原件:, cloned.self ! original); cloned.nested.deep.value 999; console.log(修改副本不影响原件:, original.nested.deep.value 1);跑node test.mjs预期输出全是true。如果Date 类型保持是false说明你环境里structuredClone不可用且降级函数没生效如果循环引用指向自身是false说明WeakMap缓存逻辑有问题。再验证 TaoToken 审查脚本。先确认.env里的 Key 和 Base URL 填对了然后跑node review.mjs。成功的话你会看到模型返回一段结构化的审查意见比如指出「fallbackClone里ArrayBuffer.isView分支没有处理DataView」「Reflect.ownKeys拷贝 Symbol 键时目标对象可能丢失描述符」这类细节。如果请求失败先看 HTTP 状态码。401说明 Key 无效或没带上Authorization头404通常是 Base URL 写错了注意是https://taotoken.net/api后面拼/v1/chat/completions429是额度或频率限制去控制台看用量。返回体里如果choices是undefined打印完整data看error字段。实测下来用 TaoToken 做代码审查的好处是同一个 Key 可以切换不同模型对比审查结果。比如先用一个模型查循环引用再用另一个模型查类型丢失不用改任何环境变量只改请求体里的model字段。这对需要多角度验证深拷贝正确性的场景很实用。5. 本篇常见错排查401、local proxy failed、reading choices、OAuth这一节把接入过程中最容易撞的报错列出来对照着改。401 Unauthorized。最常见的原因是 Key 没读到。检查.env文件是否在项目根目录dotenv/config是否在fetch之前 import。如果你用的是import dotenv/config它必须在其他代码执行前加载。另一个原因是 Key 前后有空格或换行process.env.TAOTOKEN_API_KEY.trim()一下。还有一种情况是 Key 被吊销了去 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite 重新生成一个。local proxy failed。这个报错通常出现在你本地配了代理工具但代理没启动或端口不对。TaoToken 的 API 是直连的不需要额外代理。检查你的 shell 里有没有HTTP_PROXY、HTTPS_PROXY环境变量有的话临时unset掉再跑。Node 的fetch会读这些变量如果代理地址失效就会报local proxy failed。Cannot read properties of undefined (reading choices)。这说明data.choices是undefined请求返回了错误结构。先console.log(JSON.stringify(data, null, 2))看完整返回。常见原因是model字段填了一个不存在的模型 ID或者请求体 JSON 格式错了比如多了逗号。确认模型 ID 从 https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_contentmodel-chatutm_campaignrewrite 复制别手打。OAuth 相关报错。如果你用 Claude Code 或类似工具它可能默认走 OAuth 登录流程而不是 API Key。这时候需要在工具的配置里显式指定用 API Key 模式把 Base URL 改成https://taotoken.net/apiKey 填TAOTOKEN_API_KEYModel ID 填具体模型。三件套缺一不可只填 Key 不填 Base URL 会走默认端点导致认证失败。具体配置参考 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentclaudecodeutm_campaignrewrite 。structuredClone 报 DataCloneError。这不是 TaoToken 的问题是深拷贝本身的。structuredClone遇到函数、DOM 节点、WeakMap、Error对象会抛DataCloneError。我的工具函数里用try...catch捕获后降级到递归实现但递归实现也克隆不了函数函数直接返回原引用。如果你的数据里确实有函数得单独处理比如用Object.assign保留方法、只深拷贝数据字段。循环引用导致栈溢出。报错是RangeError: Maximum call stack size exceeded。这说明你的递归实现没有用WeakMap记录已拷贝对象。检查fallbackClone里seen.set(obj, clone)是否在递归之前执行顺序反了就会无限递归。6. 语义一致 CTA把深拷贝审查接进你的日常流程深拷贝这个事写对一次不难难的是每次改代码都不漏边界。我的做法是把上面那份deepClone.js和review.mjs放进项目scripts/目录每次改完深拷贝逻辑就跑一遍测试加 AI 审查。测试脚本验证行为AI 审查查边界两层保险。如果你也想把这套流程跑起来先去 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite 拿 Key然后照着 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 里的示例把review.mjs调通。想先试模型效果用 https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_contentmodel-chatutm_campaignrewrite 在线对话把深拷贝代码贴进去让模型挑毛病。如果你要长期做代码审查、跑编码 Agenthttps://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite 的额度更适合批量任务。最后留一个我踩过的坑structuredClone在 Node 17 以下不可用如果你的 CI 跑在旧版本 Node 上supportsStructuredClone会返回false自动走降级函数。降级函数里ArrayBuffer.isView分支记得处理DataViewnew obj.constructor(obj)对DataView不生效得单独写new DataView(obj.buffer.slice(0))。这个细节我是被 AI 审查提醒才发现的你也可以让模型帮你查一遍。
返回列表