A user_4a13383d 2026-09-04 03:08 3 次浏览 Javascript 深拷贝代码实现 深拷贝不是“把对象再写一遍”,而是让新对象和原对象真正分家。本文先写出能跑的版本,再故意踩几个坑,最后补上边界处理。 ## 目录 * [一、先理解深拷贝要解决什么](#%E4%B8%80%E5%85%88%E7%90%86%E8%A7%A3%E6%B7%B1%E6%8B%B7%E8%B4%9D%E8%A6%81%E8%A7%A3%E5%86%B3%E4%BB%80%E4%B9%88) * [二、最小实现:先把主线跑通](#%E4%BA%8C%E6%9C%80%E5%B0%8F%E5%AE%9E%E7%8E%B0%E5%85%88%E6%8A%8A%E4%B8%BB%E7%BA%BF%E8%B7%91%E9%80%9A) * [三、边界来了:循环引用和继承属性](#%E4%B8%89%E8%BE%B9%E7%95%8C%E6%9D%A5%E4%BA%86%E5%BE%AA%E7%8E%AF%E5%BC%95%E7%94%A8%E5%92%8C%E7%BB%A7%E6%89%BF%E5%B1%9E%E6%80%A7) * [四、修复版:用 `WeakMap` 记录拷贝关系](#%E5%9B%9B%E4%BF%AE%E5%A4%8D%E7%89%88%E7%94%A8-weakmap-%E8%AE%B0%E5%BD%95%E6%8B%B7%E8%B4%9D%E5%85%B3%E7%B3%BB) * [五、不同方案怎么选](#%E4%BA%94%E4%B8%8D%E5%90%8C%E6%96%B9%E6%A1%88%E6%80%8E%E4%B9%88%E9%80%89) * [六、总结](#%E5%85%AD%E6%80%BB%E7%BB%93) ## 一、先理解深拷贝要解决什么 对象变量保存的不是对象本身,而是指向对象的引用。浅拷贝只复制第一层,嵌套对象仍然共用同一个引用: ```javascript const original = { user: { name: 'Ada' }, }; const shallow = { ...original }; shallow.user.name = 'Grace'; console.log(original.user.name); // Grace ``` 这就像复印了文件夹封面,里面的文件仍然是同一份。深拷贝要做的是:嵌套对象也创建新的实例,修改副本时不影响原对象。 先说清楚一个范围:本文的手写实现面向普通对象、数组以及常见内置对象。函数通常保留原引用,因为复制函数并不能复制它的闭包环境;`WeakMap`、`WeakSet` 等不可枚举内部数据的对象也需要单独制定策略。 ## 二、最小实现:先把主线跑通 为了先看懂递归的主线,暂时只处理普通对象、数组和可枚举的字符串属性: ```javascript function simpleClone(value) { if (value === null || typeof value !== 'object') { return value; } const clone = Array.isArray(value) ? [] : {}; for (const key of Object.keys(value)) { clone[key] = simpleClone(value[key]); } return clone; } const source = { name: 'Ada', settings: { theme: 'dark' }, }; const copy = simpleClone(source); copy.settings.theme = 'light'; console.log(source.settings.theme); // dark console.log(copy.settings.theme); // light ``` 这段代码做了三件事: 1. 基本数据类型直接返回,因为它们本身就是值。 2. 对象和数组创建新的容器。 3. 递归复制每个嵌套属性。 主线很清楚,但它还没有资格进入生产环境。接下来让几个边界条件来敲门。 ## 三、边界来了:循环引用和继承属性 ### 1\. 循环引用让递归停不下来 对象可以指回自己,或者多个对象互相指向: ```javascript const source = { name: 'Ada' }; source.self = source; simpleClone(source); // RangeError: Maximum call stack size exceeded ``` `simpleClone` 每次看到 `self` 都以为遇到了一个新对象,于是不断递归。问题不是递归本身,而是没有记住“这个对象已经处理过了”。 ### 2. `for...in` 可能把原型上的属性也带走 如果把遍历改成 `for...in`,它会遍历可枚举的字符串属性,包括原型链上的属性: ```javascript const proto = { inherited: '来自原型' }; const source = Object.create(proto); source.own = '来自对象自身'; const copy = {}; for (const key in source) { copy[key] = source[key]; } console.log(copy); // { own: '来自对象自身', inherited: '来自原型' } ``` 深拷贝通常只应该复制对象自己的属性。除此之外,`Object.keys` 还会漏掉 `Symbol` 属性和不可枚举属性。边界问题已经凑齐,开始修复。 ## 四、修复版:用 `WeakMap` 记录拷贝关系 `WeakMap` 在这里像一张“旧地址 → 新地址”的登记表: * 第一次遇到对象时,先创建副本并登记。 * 再次遇到同一个对象时,直接返回已登记的副本。 * 这样既能处理循环引用,也能保持共享引用关系。 ```javascript function deepClone(value, seen = new WeakMap()) { // 基本类型和函数保留原值或原引用 if (value === null || typeof value !== 'object') { return value; } if (seen.has(value)) { return seen.get(value); } if (value instanceof Date) { const clone = new Date(value); seen.set(value, clone); return clone; } if (value instanceof RegExp) { const clone = new RegExp(value.source, value.flags); clone.lastIndex = value.lastIndex; seen.set(value, clone); return clone; } if (value instanceof Map) { const clone = new Map(); seen.set(value, clone); for (const [key, item] of value) { clone.set(deepClone(key, seen), deepClone(item, seen)); } return clone; } if (value instanceof Set) { const clone = new Set(); seen.set(value, clone); for (const item of value) { clone.add(deepClone(item, seen)); } return clone; } if (value instanceof WeakMap || value instanceof WeakSet) { throw new TypeError('WeakMap 和 WeakSet 的内部数据不可遍历'); } // 保留原型;null 原型对象也能正确复制 const clone = Array.isArray(value) ? [] : Object.create(Object.getPrototypeOf(value)); seen.set(value, clone); // Reflect.ownKeys 包含字符串键、Symbol 键和不可枚举键 for (const key of Reflect.ownKeys(value)) { const descriptor = Object.getOwnPropertyDescriptor(value, key); // 访问器属性没有 value,直接保留 getter/setter 引用 if ('value' in descriptor) { descriptor.value = deepClone(descriptor.value, seen); } Object.defineProperty(clone, key, descriptor); } return clone; } ``` ### 用边界数据验证修复结果 ```javascript const token = Symbol('token'); const proto = { inherited: '原型属性' }; const source = Object.create(proto); source.user = { name: 'Ada' }; source.self = source; source[token] = { role: 'admin' }; Object.defineProperty(source, 'hidden', { value: { visible: false }, enumerable: false, writable: true, }); const copy = deepClone(source); console.log(copy !== source); // true console.log(copy.user !== source.user); // true console.log(copy.self === copy); // true console.log(copy[token] !== source[token]); // true console.log(Object.getPrototypeOf(copy) === proto); // true console.log(Object.propertyIsEnumerable.call(copy, 'hidden')); // false ``` 这里有两个容易忽略的细节: * `Reflect.ownKeys` 负责“找全属性”,属性描述符负责“原样保留可枚举性、只读性和访问器”。 * 先把副本放进 `WeakMap`,再递归子属性。顺序反了,循环引用还是会把递归栈撑爆。 这份实现并不声称能复制 JavaScript 的一切对象。`ArrayBuffer`、TypedArray、跨窗口对象和带外部资源的实例需要额外分支;遇到这些类型时,应优先确认业务需要的语义,而不是盲目追求“什么都能拷贝”。 ## 五、不同方案怎么选 \| 方案 \| 循环引用 \| 常见内置对象 \| 函数 \| 适合场景 \| \| \-\-\- \| \-\-\- \| \-\-\- \| \-\-\- \| \-\-\- \| \| `Object.assign` / 展开运算符 \| 不处理 \| 浅拷贝 \| 保留引用 \| 只复制第一层 \| \| `JSON.parse(JSON.stringify())` \| 不支持 \| 会丢失或变形 \| 丢失 \| 结构简单且明确是 JSON 数据 \| \| `structuredClone` \| 支持 \| 支持大多数可克隆类型 \| 不支持 \| 现代浏览器和 Node\.js 的首选 \| \| `_.cloneDeep` \| 支持 \| 覆盖面较广 \| 按库规则处理 \| 工程项目需要成熟实现 \| \| 手写 `deepClone` \| 可以支持 \| 需要逐类实现 \| 通常保留引用 \| 学习原理或有定制需求 \| 如果运行环境支持 `structuredClone`,优先使用它;手写版本更适合学习递归、引用关系和属性描述符,或者处理非常明确的业务类型。 ## 六、总结 * 浅拷贝只复制第一层,嵌套对象仍然共享引用。 * 递归深拷贝必须用 `WeakMap` 处理循环引用和共享引用。 * 只复制对象自身属性时,优先考虑 `Reflect.ownKeys`,不要无脑使用 `for...in`。 * `Date`、`RegExp`、`Map`、`Set` 等对象有自己的内部状态,不能用普通对象分支硬套。 * 能用 `structuredClone` 时优先用标准 API;手写实现要明确支持范围和限制。 点赞0 收藏0
评论
还没有评论,来分享你的想法吧。