JavaScript递归复制含Error对象的结构难点在于其属性不可枚举且原型特殊;需用Object.getOwnPropertyNames获取所有自有属性,单独处理message、name、stack等,并通过Object.defineProperty和setPrototypeOf确保正确克隆。

JavaScript 中递归复制包含 Error 对象的复杂结构,难点不在递归本身,而在于 Error 实例是不可枚举、不可遍历的——直接用 for...in 或 Object.keys() 会漏掉 message、stack、name 等关键属性,且其原型链特殊,普通深拷贝(如 JSON.parse(JSON.stringify()))会彻底丢失。
识别并单独处理 Error 实例
递归前先判断当前值是否为 Error 实例(注意不能只用 instanceof Error,因为跨 iframe 时可能失效;更稳妥的是检查 val.constructor === Error || val instanceof Error || (typeof val === 'object' && val !== null && typeof val.message === 'string' && typeof val.stack === 'string'))。一旦确认,就手动构造新 Error 并复制核心属性:
-
必须复制:
message、name、stack(若存在),其他自定义属性(如code、status)也需遍历Object.getOwnPropertyNames(val)获取 -
避免直接赋值
stack:某些环境(如 V8)中stack是只读访问器,应使用Object.defineProperty设置,或改用new Error(message).stack = originalStack(不推荐,兼容性差),更稳妥是新建后通过非标准方式“注入”(见下条) -
兼容方案:创建新
Error后,用Object.assign({}, original)获取所有自有属性,再逐个赋给新实例(包括stack),最后用Object.setPrototypeOf(newErr, original.constructor.prototype)恢复原型
递归时绕过不可枚举与原型干扰
标准对象遍历(for...in)会跳过 Error 的不可枚举属性,也要防止把 Error.prototype 上的方法误当数据复制。正确做法是:
- 对任意对象,统一用
Object.getOwnPropertyNames(val)获取**所有自有属性名**(含不可枚举的message、stack) - 用
Object.getOwnPropertyDescriptor(val, key)判断属性是否可写、是否为 accessor(如某些环境的stack),决定如何复制 - 跳过原型链上的属性:不调用
Object.getPrototypeOf(val)向上遍历,只处理当前对象自身属性 - 对函数、日期、正则等特殊类型,按需单独处理(例如
Date用new Date(val.getTime()))
完整递归克隆函数示例
以下是一个轻量、可扩展的递归克隆函数,专为支持 Error 设计:
立即学习“Java免费学习笔记(深入)”;
function deepClone(obj) {
if (obj === null || typeof obj !== 'object') return obj;
// 单独处理 Error
if (obj instanceof Error) {
const cloned = Object.assign(
Object.create(Object.getPrototypeOf(obj)),
{ message: obj.message, name: obj.name }
);
// 复制所有自有属性(含 stack 和自定义字段)
Object.getOwnPropertyNames(obj).forEach(key => {
if (key !== 'message' && key !== 'name') {
const desc = Object.getOwnPropertyDescriptor(obj, key);
if (desc) Object.defineProperty(cloned, key, desc);
}
});
return cloned;
}
// 处理数组
if (Array.isArray(obj)) {
return obj.map(item => deepClone(item));
}
// 处理普通对象
const result = {};
Object.getOwnPropertyNames(obj).forEach(key => {
const desc = Object.getOwnPropertyDescriptor(obj, key);
if (desc && (desc.get || desc.set)) {
Object.defineProperty(result, key, desc);
} else {
result[key] = deepClone(obj[key]);
}
});
// 恢复原型(重要!否则 instanceof Error 会失败)
Object.setPrototypeOf(result, Object.getPrototypeOf(obj));
return result;
}
使用示例:const err = new TypeError('bad input'); err.code = 'E_BAD'; err.stack = 'custom stack'; const cloned = deepClone({ a: 1, err }); console.log(cloned.err instanceof TypeError); // true
注意事项与边界情况
实际使用中还需考虑:
-
循环引用:上述代码未处理。如需支持,需加
WeakMap缓存已克隆对象,每次递归前查表 -
Symbol 属性:用
Object.getOwnPropertySymbols(obj)补充获取,并同样用defineProperty复制 -
DOM 节点、Web API 对象(如
File、Promise):无法真正克隆,应明确抛错或返回占位符 -
性能:频繁调用时,避免重复
getOwnPropertyNames和getOwnPropertyDescriptor;可预判常见类型(如已知是Error)提前分支


















