⭐ 推荐:社区规则条款 V1.0

js 深拷贝和浅拷贝怎么实现

GJ论坛站长
GJ论坛站长 管理员资深会员 👑黑卡会员
发布于 2026-10-18 11:45 ·3 浏览 ·0 回复
内容摘要

浅拷贝仅复制第一层属性、嵌套对象仍共享引用,可用 Object.assign 或展开运算符;深拷贝递归复制所有层级,优先用 structuredClone,兼容旧环境或克隆函数、类实例时用手写递归或 lodash.cloneDeep。

结论:浅拷贝只复制第一层属性,嵌套对象仍指向同一块内存,用 Object.assign 或展开运算符就能实现;深拷贝会递归复制所有层级,生产环境首选 structuredClone()(Chrome 98+/Node 17+),需要兼容旧浏览器或要克隆函数、自定义类实例时,再手写递归或上 lodash.cloneDeep。

浅拷贝和深拷贝到底有什么区别

区别只有一句话:拷贝后修改嵌套对象,原对象会不会跟着变。浅拷贝不会复制引用类型属性指向的对象,只复制引用地址;深拷贝会把每一层引用类型都新建一份。

const a = { name: 'x', info: { age: 1 } };
const shallow = { ...a };
shallow.info.age = 2;
console.log(a.info.age); // 2,原对象被改动

栈内存里存的是基本类型值和引用地址,堆内存里才是对象本体。浅拷贝只处理栈这一层,深拷贝要一直走到堆的末端。这是判断一个拷贝是深还是浅的唯一标准,跟用哪个 API 无关。

浅拷贝有哪几种写法

结论:Object.assign({}, obj)、{...obj}、[...arr]、arr.slice()、arr.concat() 都是浅拷贝,效果等价,日常用展开运算符最简洁。

三个必知的坑:

  1. Object.assign 和展开运算符只复制自有可枚举属性,原型链上的属性和不可枚举属性(比如 Date 的内部槽)会丢。{...new Date()} 得到的是 {},不是日期。
  2. Object.assign(target, source) 会触发 target 上同名属性的 setter,展开运算符不会。所以 Object.assign 不能安全地用来合并带 setter 的类实例。
  3. 两者都会复制 Symbol 类型的可枚举属性,这点比 for...in 靠谱。

JSON.parse(JSON.stringify()) 为什么不能算通用深拷贝

结论:它只对「纯 JSON 数据」有效,遇到 undefined、函数、Symbol、Date、RegExp、Map、Set、循环引用中的任意一个都会出错或失真。

具体失真清单:undefined、函数、Symbol 属性被直接丢弃;NaN、Infinity 变成 null;Date 变成 ISO 字符串;RegExp 变成 {};Map/Set 变成 {};BigInt 直接抛 TypeError;循环引用抛 TypeError: Converting circular structure to JSON。性能上它还要先序列化再反序列化,大对象比递归慢。

能用的场景很窄:接口返回的纯数据对象,字段只有 string、number、boolean、null、数组和普通对象。

手写递归深拷贝怎么写

结论:递归实现必须解决三件事:处理循环引用、区分内置类型、保留原型。核心是用 WeakMap 记录已克隆对象,且必须在递归子属性之前写入。

function deepClone(target, map = new WeakMap()) {
  if (target === null || typeof target !== 'object') return target;
  if (map.has(target)) return map.get(target);      // 循环引用
  const type = Object.prototype.toString.call(target);
  if (type === '[object Date]') return new Date(target);
  if (type === '[object RegExp]') return new RegExp(target.source, target.flags);

  let clone;
  if (type === '[object Map]') {
    clone = new Map(); map.set(target, clone);
    target.forEach((v, k) => clone.set(deepClone(k, map), deepClone(v, map)));
    return clone;
  }
  if (type === '[object Set]') {
    clone = new Set(); map.set(target, clone);
    target.forEach(v => clone.add(deepClone(v, map)));
    return clone;
  }
  clone = Array.isArray(target) ? [] : Object.create(Object.getPrototypeOf(target));
  map.set(target, clone);                            // 先登记,再递归
  Reflect.ownKeys(target).forEach(k => {
    clone[k] = deepClone(target[k], map);
  });
  return clone;
}

关键点:map.set(target, clone) 放在遍历子属性之前,否则 a.self = a 这种结构会栈溢出;用 Reflect.ownKeys 而不是 Object.keys,才能带上 Symbol 键和不可枚举键;Object.create(Object.getPrototypeOf(target)) 保住原型链,让 instanceof 依然成立。

structuredClone 能替代手写深拷贝吗

结论:能覆盖 90% 的业务场景,而且它由引擎原生实现,性能和正确性都优于手写版本,但有两类东西它拷不了。

支持范围:循环引用、Map、Set、Date、RegExp、ArrayBuffer、TypedArray、Blob、File、Error。

不支持范围:函数、Symbol、DOM 节点会直接抛 DataCloneError;自定义类的实例会被降级成普通对象,原型丢失;getter/setter 会被求值成普通属性,属性描述符不保留。

运行环境要求:Chrome 98、Firefox 94、Safari 15.4、Node.js 17 起原生可用。

实际项目里到底选哪个

结论:按数据形态选,不要一律上递归。

  • 纯 JSON 数据、只在乎值:structuredClone(),一行搞定。
  • 目标环境可能低于 Node 17 或需要跑在旧版移动端 WebView:lodash.cloneDeep,它内部处理了循环引用和绝大多数内置类型,是目前最稳的第三方方案。
  • 对象里有函数、需要保留自定义类原型、需要精确控制哪些字段不拷贝:手写递归,并把 WeakMap 和 Reflect.ownKeys 这两点写对。
  • 需要保证不可变数据、又嫌深拷贝开销大:改用 immer 这类结构共享方案,只重建被修改的路径,而不是整棵树。

记住一条判断线:只要被拷贝对象里存在引用类型的嵌套,就必须确认你用的是深拷贝;只写 {...obj} 却以为已经隔离,是线上数据被意外篡改最常见的来源。

本文转载自 GJ站长论坛,原文地址:https://www.gj0.com/thread-2264.html
转载请注明出处,版权归原作者所有。
—/ 5暂无评分
还没有评分,来打第一分吧

全部回复 0

还没有回复,来抢沙发~