在JavaScript开发中,对象克隆是一项看似简单却隐藏着诸多陷阱的操作。许多开发者习惯性地依赖Object.assign(),认为它能生成一个“干净”的副本。但实际效果真的如此理想吗?

直接使用Object.assign()来完成严谨的原始对象克隆,这条路并不可行。它本质上是浅拷贝工具,会忽略对象的不可枚举属性、原型链、Symbol键(除非你手动列出)、getter/setter访问器,以及Date、RegExp等特殊内置对象。配合Reflect API虽然能增强对对象元信息的探测能力,但Reflect本身并不提供深拷贝或属性控制逻辑。要实现接近“严谨”的克隆,关键在于先明确克隆目标——是否需要保留原型?是否要处理Symbol属性?是否要跳过访问器?——然后借助Reflect进行辅助探测,最后利用Object.assign或更底层的操作执行最终赋值。
识别并复制所有自有属性(含不可枚举和Symbol)
默认情况下,Object.assign()仅复制可枚举的自有字符串键属性。若要覆盖更全面的属性集合,需要先通过Reflect.ownKeys()获取对象所有自有键,包括Symbol键和不可枚举的字符串键。拿到完整的键数组后,再逐个判断和复制:
- 首先,使用
Reflect.ownKeys(obj)获取完整的键数组。 - 接着,对数组中的每个键
key,通过Object.getOwnPropertyDescriptor(obj, key)获取其属性描述符,以此判断该属性是普通数据属性(存在value且非访问器)还是访问器属性。 - 如果需要保留属性的原始特性(如
writable可写性、configurable可配置性),就不能简单地用Object.assign()赋值,而应改用Object.defineProperty()精确定义目标对象的属性。
谨慎处理accessor属性(getter/setter)
这是Object.assign()的一个典型“陷阱”:当遇到accessor属性(即getter/setter)时,它不会复制访问器逻辑本身,而是调用getter,将返回值作为普通数据属性值写入目标对象,导致原有的getter/setter逻辑丢失。若要保留访问器,必须显式检测:
- 通过
Object.getOwnPropertyDescriptor()检查属性描述符中是否存在get或set函数。 - 如果存在,应使用
Object.defineProperty(target, key, descriptor)将整个描述符对象复制过去,而非仅复制value。 - 需注意:直接复制accessor可能引发副作用,例如原getter函数绑定了
this或包含副作用逻辑,在业务场景中需评估是否可接受。
决定是否保留原型链
Object.assign()总是将属性复制到一个全新的普通对象({})上,该新对象默认继承自Object.prototype,导致原始对象的原型链信息完全丢失。如果你的克隆需求包括保留原型,就需要调整目标对象的创建方式:
- 应使用
Object.create(Object.getPrototypeOf(obj))创建目标对象clone,这样clone就继承了原对象的原型。 - 后续所有属性复制操作(包括处理Symbol、accessor等)都作用在这个
clone对象上。 - 另外,若原对象的原型链上存在不可扩展或被冻结的对象,后续使用
Object.defineProperty时可能抛出错误,操作前最好用Object.isExtensible()检查。
避开常见陷阱:Date、RegExp、Map、Set等内置对象
无论是Object.assign()还是Reflect,都无法自动识别并正确克隆JavaScript的内置特殊对象。例如:
- 一个
new Date()实例被克隆后可能变成一个空对象{},因为Date实例内部存储的[[PrimitiveValue]]并不作为自有属性暴露。 new Map()或new Set()实例中存储的键值对或元素,完全无法通过ownKeys或getOwnPropertyDescriptor获取。- 解决方案是在克隆前进行类型检测(例如
obj instanceof Date、obj.constructor === Map等),对于这些已知特殊类型,手动创建新实例并还原其内容。
道理并不复杂,却很容易被忽略。说到底,实现一个真正严谨的克隆,不能依赖某个单一API。它更像一个根据具体需求定制的策略:用Reflect.ownKeys和getOwnPropertyDescriptor进行全面探查,用Object.defineProperty进行精确的属性定义控制,用Object.create来保持原型链,最后再对特殊内置类型进行单独的兜底处理。在这个过程中,Object.assign()只适合最简单的场景,实在不宜作为“严谨克隆”的主力工具。
