深拷贝默认丢失Symbol键,需用Object.getOwnPropertyNames().concat(Object.getOwnPropertySymbols())获取全部键,递归时直接透传Symbol键并深拷其值,WeakMap处理循环引用不影响Symbol键。

深拷贝时默认会丢失 Symbol 键,因为 Object.keys()、Object.getOwnPropertyNames() 都不返回 Symbol 属性,必须显式调用 Object.getOwnPropertySymbols() 才能获取。要支持 Symbol 键的克隆,核心是:遍历所有键(包括 Symbol),并递归处理每个值。
获取对象全部键(字符串 + Symbol)
普通对象的键分两类:字符串键和 Symbol 键。深拷贝前需合并两者:
-
Object.keys(obj)获取可枚举字符串键 -
Object.getOwnPropertyNames(obj)获取所有字符串键(含不可枚举) -
Object.getOwnPropertySymbols(obj)获取所有 Symbol 键(含不可枚举)
推荐用 Object.getOwnPropertyNames(obj).concat(Object.getOwnPropertySymbols(obj)),它覆盖所有自有属性(无论是否可枚举),且不依赖 for...in(后者跳过不可枚举和 Symbol 键)。
递归拷贝时保留 Symbol 键的类型和值
Symbol 是原始值,但每个 Symbol 都是唯一值(Symbol('a') !== Symbol('a'))。若需“克隆” Symbol 键,实际只能复用原 Symbol(即浅拷贝该键),不能生成新 Symbol —— 否则就不是同一个键了。因此拷贝逻辑中,对 Symbol 类型的 key 直接透传,不转换、不重命名。
示例片段:
const symbols = Object.getOwnPropertySymbols(source);
symbols.forEach(sym => {
target[sym] = deepClone(source[sym]); // 保持 sym 不变,只深拷其值
});
处理循环引用 + Symbol 键兼容
循环引用检测通常用 WeakMap 缓存已拷贝对象,key 为原对象,value 为对应克隆体。由于 WeakMap 的 key 只接受对象,而 Symbol 是原始值,不影响缓存逻辑 —— 你只需确保在遍历键时,对 Symbol 键的访问不干扰 WeakMap 的 key 判断即可。正常实现中,WeakMap 仅用于对象层级的引用追踪,与键类型无关。
关键点:
- 每次进入对象拷贝前,先查 WeakMap 是否已有该 source 的克隆体
- 若有,直接返回,避免无限递归
- Symbol 键的值若也是对象,其内部循环引用仍会被 WeakMap 捕获
完整轻量实现(ES6+)
以下是一个支持 Symbol 键、循环引用、常见类型的深拷贝函数:
function deepClone(obj, seen = new WeakMap()) {
if (obj === null || typeof obj !== 'object') return obj;
if (seen.has(obj)) return seen.get(obj);
let cloned;
if (Array.isArray(obj)) {
cloned = [];
} else if (obj instanceof Date) {
cloned = new Date(obj.getTime());
} else if (obj instanceof RegExp) {
cloned = new RegExp(obj);
} else {
cloned = Object.create(Object.getPrototypeOf(obj));
}
seen.set(obj, cloned);
// 处理所有自有属性:字符串键 + Symbol 键
const keys = Object.getOwnPropertyNames(obj).concat(Object.getOwnPropertySymbols(obj));
for (const key of keys) {
const descriptor = Object.getOwnPropertyDescriptor(obj, key);
// 保持属性特性(如 writable、enumerable、configurable)
Object.defineProperty(
cloned,
key,
{
value: deepClone(descriptor.value, seen),
writable: descriptor.writable,
enumerable: descriptor.enumerable,
configurable: descriptor.configurable
}
);
}
return cloned;
}
该实现保留属性描述符,确保不可枚举 Symbol 键、冻结属性、getter/setter 等行为尽可能一致。如无需高保真,可简化为直接赋值 cloned[key] = deepClone(obj[key], seen),但会丢失不可枚举性和属性特性。


















