body-upload-js

JavaScript 深拷贝的正确姿势:从 JSON.stringify 到 structuredClone

拷贝一个对象,听起来是再基础不过的操作。但在 JavaScript 里,"拷贝"这个词下面埋着一堆坑:浅拷贝和深拷贝的区别、JSON.parse(JSON.stringify(x)) 这个经典写法的边界、以及不同场景下到底该用哪个 API。这篇文章把这几个问题一次讲清楚,并给出可以跑通的代码示例。

适合对象:日常写前端或 Node.js、需要复制复杂对象(嵌套对象、数组、DateMapSet 等)的开发者。

一、先分清:赋值、浅拷贝、深拷贝

这三者的差异,用一个嵌套对象最能直观说明:

js 复制代码
const source = {
  name: "alice",
  address: { city: "Shanghai", zip: "200000" },
};

// 1. 赋值:两个变量指向同一个对象
const byRef = source;
byRef.address.city = "Beijing";
console.log(source.address.city); // "Beijing" ------ source 被一起改了

// 2. 浅拷贝:只复制第一层,嵌套对象仍是共享引用
const shallow = { ...source };
shallow.address.city = "Shenzhen";
console.log(source.address.city); // "Shenzhen" ------ 嵌套对象又被改了
console.log(shallow.name === source.name); // true(字符串,值相等)

// 3. 深拷贝:嵌套对象也完全独立
const deep = structuredClone(source);
deep.address.city = "Hangzhou";
console.log(source.address.city); // 仍是 "Shenzhen",互不影响

结论一句话:浅拷贝只断开第一层引用,深拷贝递归地断开所有层级的引用。

常见的浅拷贝写法有:展开运算符 { ...obj }Object.assign({}, obj)、数组的 slice() / concat() / [...arr]。它们处理一层够用,遇到嵌套结构就失效了。

二、JSON 方案:好用,但有一大堆例外

最流行的深拷贝写法大概是这个:

js 复制代码
const copy = JSON.parse(JSON.stringify(obj));

它的优点就一条:写起来短,且对纯粹的 JSON 数据(字符串、数字、布尔、null、数组、普通对象)完全够用。但它的缺点一抓一大把:

1. 丢类型信息。 任何不是 JSON 原生支持的值,都会在序列化过程中被悄悄改变:

js 复制代码
const data = {
  date: new Date("2026-01-01T00:00:00Z"),
  undef: undefined,
  fn: function () { return 1; },
  nan: NaN,
  inf: Infinity,
  sym: Symbol("id"),
};

const copy = JSON.parse(JSON.stringify(data));
console.log(copy.date);        // "2026-01-01T00:00:00.000Z" ------ 变成了字符串,不再是 Date
console.log(copy.undef);       // 属性被直接丢弃(undefined、函数、Symbol 都不参与序列化)
console.log("fn" in copy);     // false
console.log(copy.nan);         // null ------ NaN/Infinity 变成 null

2. 环引用直接报错。 对象里如果有自引用,JSON.stringify 会抛异常:

js 复制代码
const circular = { name: "x" };
circular.self = circular;
try {
  JSON.stringify(circular);
} catch (e) {
  console.log(e.message); // "Converting circular structure to JSON"
}

3. MapSetArrayBuffer 等现代结构会被破坏。 Map 序列化后变成空对象 {}Set 变成 {},这是最容易踩的隐性 bug:

js 复制代码
const m = new Map([["k", 1]]);
const cloned = JSON.parse(JSON.stringify(m));
console.log(cloned); // {} ------ 数据丢了

所以 JSON.parse(JSON.stringify()) 只适合明确已知是纯 JSON 数据 的场景,比如接口返回的响应对象。凡是可能含有 DateMapSet、环引用、函数的对象,都别用。

三、structuredClone:现代标准答案

从 2022 年起,浏览器和 Node.js(≥17)都原生支持 structuredClone()。它是对手写递归方案的官方替代,语义明确。

js 复制代码
const original = {
  name: "alice",
  date: new Date("2026-01-01T00:00:00Z"),
  map: new Map([["k", 1]]),
  set: new Set([1, 2, 3]),
  buffer: new ArrayBuffer(8),
  nested: { list: [1, 2, 3] },
};

const copy = structuredClone(original);
copy.map.set("k", 99);
copy.date.setFullYear(2030);

console.log(original.map.get("k")); // 1 ------ 完全独立
console.log(original.date.getFullYear()); // 2026
console.log(copy.buffer instanceof ArrayBuffer); // true ------ 二进制数据也支持

structuredClone 支持的场景远超 JSON 方案:

  • DateMapSetArrayBufferBlobRegExpTypedArray 等都能正确克隆;

  • 支持环引用

    js 复制代码
    const circ = { name: "x" };
    circ.self = circ;
    const copy = structuredClone(circ);
    console.log(copy.self === copy); // true ------ 环被保留了

但它也有两个明确的"不支持":

  1. 函数 ------structuredClone 无法克隆函数,会抛 DataCloneError

    js 复制代码
    try {
      structuredClone({ fn: () => 1 });
    } catch (e) {
      console.log(e.name); // "DataCloneError"
    }
  2. 类实例的原型链 ------克隆结果是普通对象,instanceof 判断会失效,自定义类的私有字段也不会被复制。

如果你的对象就是"数据"而非"带方法的对象",structuredClone 是现在性价比最高的选择。

兼容性提示structuredClone 在 Chrome 98+、Firefox 94+、Safari 15.4+、Node 17+ 可用。需要兼容更老环境时,可以用 core-js 的 polyfill 或退回手写递归,但 Node 端直接升级版本通常更省事。

四、手写递归拷贝:理解原理,也用于兜底

了解原理有助于判断边界。一个能处理嵌套对象、数组、DateMapSet、环引用的实现大致如下:

js 复制代码
function deepClone(value, seen = new Map()) {
  // 基本类型直接返回
  if (value === null || typeof value !== "object") {
    return value;
  }

  // 处理环引用:如果已经拷贝过这个对象,直接返回它的副本
  if (seen.has(value)) {
    return seen.get(value);
  }

  if (value instanceof Date) {
    return new Date(value.getTime());
  }
  if (value instanceof RegExp) {
    return new RegExp(value.source, value.flags);
  }
  if (value instanceof Map) {
    const copy = new Map();
    seen.set(value, copy);
    for (const [k, v] of value) {
      copy.set(deepClone(k, seen), deepClone(v, seen));
    }
    return copy;
  }
  if (value instanceof Set) {
    const copy = new Set();
    seen.set(value, copy);
    for (const v of value) {
      copy.add(deepClone(v, seen));
    }
    return copy;
  }

  if (Array.isArray(value)) {
    const copy = [];
    seen.set(value, copy);
    for (const item of value) {
      copy.push(deepClone(item, seen));
    }
    return copy;
  }

  // 普通对象
  const copy = {};
  seen.set(value, copy);
  for (const key of Object.keys(value)) {
    copy[key] = deepClone(value[key], seen);
  }
  return copy;
}

验证一下:

js 复制代码
const src = {
  date: new Date(),
  map: new Map([["a", { deep: true }]]),
  nested: [1, [2, 3]],
};
src.loop = src;

const dst = deepClone(src);
console.log(dst.loop === dst);       // true(环被正确保留)
console.log(dst.map.get("a") !== src.map.get("a")); // true(深拷贝生效)
console.log(dst.date.getTime() === src.date.getTime()); // true

这段代码用 seen(一个 Map)记录"原对象 → 新副本"的映射,遇到已经处理过的对象直接返回之前的副本,从而解决环引用。

它的局限 :这段实现用 Object.keys 遍历普通对象,会忽略不可枚举属性和 Symbol 键;也没处理 ArrayBufferBlob 等二进制对象,更没有保留原型链。对于这些需求,structuredClone 是更稳妥的选择------手写版本更适合用来理解原理或做针对性定制。

五、怎么选:一张对比表

方案 适用场景 主要坑
赋值 = 就是想要共享同一份数据 改一个两个都变
浅拷贝 {...} / Object.assign 只改第一层,或明确知道没有嵌套 嵌套对象仍是共享引用
JSON.parse(JSON.stringify()) 纯 JSON 数据(接口响应等) 丢类型、丢 undefined/函数,环引用报错,Map/Set 变空
structuredClone 现代环境下的通用数据克隆(推荐) 不能克隆函数,不保留类原型
手写 deepClone 需要定制逻辑、或极老环境兜底 边界情况多,容易漏处理

小结

  • 深拷贝的本质是递归断开所有层级的引用,浅拷贝只断开第一层。
  • JSON.parse(JSON.stringify()) 只适合纯 JSON 数据,遇到 DateMapSet、环引用就会出错。
  • 现代项目里,首选 structuredClone():语义清晰、支持类型广、还能处理环引用,唯一明显短板是不支持函数和类原型。
  • 需要极致定制或兼容老环境时,才考虑手写递归实现,并充分理解它的边界。

延伸阅读:

相关推荐
Apifox16 分钟前
Apifox 8 月更新|调试、权限与协作体验持续优化
前端·后端·测试
杉氧18 分钟前
React 灵魂:深入剖析 Hooks 与副作用(Side Effects)陷阱
android·前端·react native
Json____21 分钟前
java-宿舍安全卫生检查系统项目源码
java·前端·javascript·课程设计·it学习·wwwoop.com
lichenyang45333 分钟前
鸿蒙ArkUI 键盘避让终极踩坑指南:RESIZE 无效、Column 布局遮挡、折叠屏适配全解
前端
AbelTomato37 分钟前
博客评论系统搭建回顾
javascript·typescript·cloudflare·supabase·hyperdrive
梦醒沉醉39 分钟前
4、函数function
javascript
爱勇宝43 分钟前
公司没给活干,却因为员工看手机把人开了:法院判赔11万元
前端·后端·程序员
Patrick_Wilson44 分钟前
iOS Safari 按钮点击无响应问题修复指南
前端·ios·safari
Csvn1 小时前
模块化与工程化:CJS / ESM / 打包器,到底怎么协作?
前端