题目重点:用WeakMap复制对象图,保稀疏数组和null原型,拒绝未定义的访问器及特殊对象。
题意与假设
这道练习题保留了面经中值得训练的核心约束,并将需要澄清的条件单独列出。现场拿到变体时,先和面试官确认这些条件。
- 训练支持primitive(不含function)、普通/null原型object与array,复制自有可枚举数据属性,包括symbol key。
- 不复制非枚举或原descriptor约束,不执行getter;Date/Map/Set/类实例拒绝,需扩展专门分支。
- 复制后保持图内别名与循环,引用型节点与原图隔离;symbol key自身身份保留。
解题路线
- JSON stringify/parse会丢undefined、symbol等、改变类型并对循环失败,不是通用deepclone;structuredClone有自己的类型/原型合同,也不能替代未定义的自定义要求。
- 递归遇对象先查seen,已复制则返回对应副本;首次先创建空壳并登记,再遍历属性。先递归再登记会在self循环无限。
- 共享引用也必须保留:原a.x和a.y指同一个对象,副本x/y仍应同指一份新对象;若每支独立复制,会改变身份关系并浪费内存。
- array创建同length后仅复制存在的自有可枚举属性,保留hole而非填undefined。普通object用原本Object.prototype/null两类原型,类实例方法/资源不能悄悄丢。
- 通过descriptor读取数据value而不触getter,用defineProperty写避免__proto__键触发原型setter。访问器明确拒绝,不给“执行它再复制结果”的隐性副作用。
- source自身访问可能为Proxy则仍可触发trap,本合同不保证无副作用处理所有代理。深递归需深度预算或显式栈,大图需大小预算。
- 测试以原生structuredClone对共同支持图作独立值对照,再核身份映射、循环、hole、symbol、null原型和危险键;特殊类型错误路径也验证。
手动走一遍
obj.self=obj,副本self指副本;obj.a=obj.b=child,副本a===b但不等于原child。数组只有下标3时长度4且0..2仍hole。__proto__作为数据键不能把目标原型改成其value。
JavaScript 参考实现
下面的代码按上文假设实现,适合本地运行并追加自己的测试用例。
代码区可横向滚动;使用键盘时,先聚焦代码区再按方向键查看。
export function deepClone(value, seen = new WeakMap()) {
if (value === null || typeof value !== 'object') {
if (typeof value === 'function') throw new TypeError('functions are outside this contract');
return value;
}
if (seen.has(value)) return seen.get(value);
const proto = Object.getPrototypeOf(value);
const array = Array.isArray(value);
if (!array && proto !== Object.prototype && proto !== null) {
throw new TypeError('only arrays and plain objects are supported');
}
const result = array ? new Array(value.length) : Object.create(proto);
seen.set(value, result);
for (const key of Reflect.ownKeys(value)) {
const descriptor = Object.getOwnPropertyDescriptor(value, key);
if (!descriptor.enumerable) continue;
if (!Object.hasOwn(descriptor, 'value')) throw new TypeError('accessors are outside this contract');
Object.defineProperty(result, key, {
value: deepClone(descriptor.value, seen),
enumerable: true, writable: true, configurable: true,
});
}
return result;
}
复杂度
对象图V节点E属性引用平均O(V+E)时间空间,seen避免循环重复;递归栈O(D),深度D很大可能溢出。symbol/字符串与primitive按值合同共享。
易错点与追问
容易失分的边界
- 先递归后登记seen。
- 共享引用复制成多个对象。
- hole填成undefined。
- 赋__proto__改变原型或执行getter。
继续深挖
- Date/Map/Set怎样扩展?
- descriptor是否应保留?
- 显式栈如何控制深图?
