JavaScript 中如何实现支持描述符保留的深拷贝方法

作者:袖梨 2026-07-07
JavaScript中实现支持描述符保留的深拷贝,关键在于用Object.getOwnPropertyDescriptors获取所有自有属性描述符,递归深拷贝value/get/set,再用Object.defineProperties重建;需处理循环引用、访问器this指向、Symbol键、不可枚举属性及Date/RegExp/Map/Set等特殊对象。

JavaScript 中实现支持描述符保留的深拷贝,关键在于不能只复制属性值,还要完整还原 Object.defineProperty 所定义的所有特性(如 enumerablewritableconfigurableget/set)。JSON 序列化、structuredClone 或简单递归赋值都无法保留这些元信息。

使用 Object.getOwnPropertyDescriptors + 递归重建

这是最直接可控的方式:先用 Object.getOwnPropertyDescriptors 获取源对象所有自有属性的完整描述符,再对每个描述符中的 valueget/set 函数做深拷贝,最后用 Object.defineProperties 在新对象上重建。

  • 对普通值(基本类型、null、undefined)直接返回
  • 对函数,通常不深拷贝(函数无法真正“复制”,且闭包不可序列化),可选择原样保留或浅拷贝引用
  • 对对象/数组,递归处理其自身属性描述符;注意避免循环引用,需用 WeakMap 记录已拷贝对象
  • get/set,需重新包装——内部访问的 this 要指向新对象,因此不能直接赋值,要重写访问器逻辑

处理访问器属性(getter/setter)的细节

直接复制 get 函数会导致它内部的 this 仍指向原对象。正确做法是:在新对象上定义新的访问器,让 get 从新对象读取对应键(若目标是普通值),或调用深拷贝后的 getter 函数(若 getter 本身依赖其他状态)。

  • 若原属性是 { get() { return this._x * 2; } },拷贝后应生成 { get() { return this._x * 2; } },其中 this._x 是新对象上的深拷贝值
  • 更稳妥的做法是:对访问器属性,仅拷贝其函数体(通过 toString()),并在新上下文中执行——但这有局限(无法处理闭包变量);生产环境建议明确约定访问器不依赖外部闭包

兼容 Symbol 键与不可枚举属性

Object.getOwnPropertyDescriptors 天然支持 Symbol 类型的键和不可枚举属性,无需额外处理。但要注意:for...inObject.keys 会忽略它们,必须坚持使用该 API 才能保全。

立即学习“Java免费学习笔记(深入)”;

  • 确保递归时对每个对象都调用 Object.getOwnPropertyDescriptors(obj),而不是 Object.keys(obj)
  • Symbol 键的 descriptor 会被正常捕获,拷贝后仍为 Symbol 键,语义完全一致
  • 不可枚举属性(enumerable: false)在拷贝后也保持不可枚举,不影响遍历行为

避开内置类与特殊对象的陷阱

Date、RegExp、Map、Set、ArrayBuffer 等类型需单独判断并实例化。它们的描述符可能包含内部槽(internal slots),无法通过 getOwnPropertyDescriptors 完全暴露。例如:

  • new Date()[[DateValue]] 不可枚举也不可配置,但可通过 getTime() 提取后重建
  • RegExp 实例的 sourceflags 可读,用 new RegExp(desc.source, desc.flags) 构造
  • Map/Set 需遍历 entries()values(),对键值对分别深拷贝后再填入新实例

不复杂但容易忽略:深拷贝描述符的本质不是“复制数据”,而是“复现定义行为”。只要每一步都基于 defineProperty 级别的控制,并小心处理函数、循环引用和宿主对象,就能可靠保留所有元信息。

相关文章

精彩推荐