JavaScript 深拷贝:从基础到完美实现
深拷贝是 JavaScript 开发中常见的需求,本文将带你从基础概念开始,逐步深入,最终实现一个完美的深拷贝函数。
一、浅拷贝 vs 深拷贝
1. 浅拷贝
浅拷贝只复制对象的第一层属性,更深层次的属性仍然是共享的。
const obj = { a: 1, b: { c: 2 } };
const shallowCopy = { ...obj };shallowCopy.b.c = 3;
console.log(obj.b.c); // 3 - 原对象也被修改了
2. 深拷贝
深拷贝会递归复制对象的所有层级,创建完全独立的副本。
const obj = { a: 1, b: { c: 2 } };
const deepCopy = JSON.parse(JSON.stringify(obj));deepCopy.b.c = 3;
console.log(obj.b.c); // 2 - 原对象不受影响
局限性:
- 不能处理函数、undefined、Symbol
- 不能处理循环引用
- 会丢失 Date 对象(转为字符串)
- 会丢失 RegExp 对象
- 会丢失原型链
2. 递归实现(基础版)
function deepClone(obj) {if (obj === null || typeof obj !== 'object') {return obj;}let clone = Array.isArray(obj) ? [] : {};for (let key in obj) {if (obj.hasOwnProperty(key)) {clone[key] = deepClone(obj[key]);}}return clone;
}
问题:无法处理循环引用
三、处理循环引用
function deepClone(obj, hash = new WeakMap()) {if (obj === null || typeof obj !== 'object') {return obj;}// 处理循环引用if (hash.has(obj)) {return hash.get(obj);}let clone = Array.isArray(obj) ? [] : {};hash.set(obj, clone);for (let key in obj) {if (obj.hasOwnProperty(key)) {clone[key] = deepClone(obj[key], hash);}}return clone;
}
四、处理特殊对象
1. 处理 Date 和 RegExp
function deepClone(obj, hash = new WeakMap()) {if (obj === null || typeof obj !== 'object') {return obj;}// 处理特殊对象if (obj instanceof Date) return new Date(obj);if (obj instanceof RegExp) return new RegExp(obj);if (hash.has(obj)) return hash.get(obj);//自定义对象处理let clone = new obj.constructor();hash.set(obj, clone);for (let key in obj) {if (obj.hasOwnProperty(key)) {clone[key] = deepClone(obj[key], hash);}}return clone;
}
2. 处理 Map 和 Set
function deepClone(obj, hash = new WeakMap()) {// ...前面的代码...if (obj instanceof Map) {const clone = new Map();hash.set(obj, clone);obj.forEach((value, key) => {clone.set(deepClone(key, hash), deepClone(value, hash));});return clone;}if (obj instanceof Set) {const clone = new Set();hash.set(obj, clone);obj.forEach(value => {clone.add(deepClone(value, hash));});return clone;}// ...后面的代码...
}
五、处理 Symbol 属性和原型链
function deepClone(obj, hash = new WeakMap()) {// 处理原始值和nullif (Object(obj) !== obj || obj === null) return obj;// 处理特殊对象if (obj instanceof Date) return new Date(obj);if (obj instanceof RegExp) return new RegExp(obj);if (obj instanceof Map) {const clone = new Map();hash.set(obj, clone);obj.forEach((value, key) => {clone.set(deepClone(key, hash), deepClone(value, hash));});return clone;}if (obj instanceof Set) {const clone = new Set();hash.set(obj, clone);obj.forEach(value => {clone.add(deepClone(value, hash));});return clone;}// 处理循环引用if (hash.has(obj)) return hash.get(obj);// 获取所有属性,包括Symbolconst allKeys = Reflect.ownKeys(obj);// 获取原型const proto = Object.getPrototypeOf(obj);// 创建新对象,保持原型链const clone = Object.create(proto);hash.set(obj, clone);// 复制所有属性allKeys.forEach(key => {clone[key] = deepClone(obj[key], hash);});return clone;
}
六、最终完美实现
function deepClone(obj, hash = new WeakMap()) {// 处理原始值和nullif (Object(obj) !== obj || obj === null) return obj;// 处理特殊对象if (obj instanceof Date) return new Date(obj);if (obj instanceof RegExp) return new RegExp(obj);if (obj instanceof Map) {const clone = new Map();hash.set(obj, clone);obj.forEach((value, key) => {clone.set(deepClone(key, hash), deepClone(value, hash));});return clone;}if (obj instanceof Set) {const clone = new Set();hash.set(obj, clone);obj.forEach(value => {clone.add(deepClone(value, hash));});return clone;}if (obj instanceof ArrayBuffer) return obj.slice(0);if (ArrayBuffer.isView(obj)) {return new obj.constructor(deepClone(obj.buffer, hash),obj.byteOffset,obj.byteLength);}// 处理循环引用if (hash.has(obj)) return hash.get(obj);// 处理普通对象和数组const proto = Object.getPrototypeOf(obj);const clone = Object.create(proto);hash.set(obj, clone);// 复制Symbol属性和普通属性Reflect.ownKeys(obj).forEach(key => {clone[key] = deepClone(obj[key], hash);});const allDesc = Object.getOwnPropertyDescriptors(obj)const result = Object.create(Object.getPrototypeOf(obj), allDesc)hash.set(obj, result)return clone;
}
WeakMap 的基本作用
WeakMap 是 JavaScript 中的一种特殊集合类型,与普通 Map 的主要区别在于:
键必须是对象(不能是原始值)
键是弱引用(不会阻止垃圾回收)
不可枚举(没有方法能获取所有键)
解决循环拷贝问题
在实现深拷贝时,遇到循环引用(对象属性间接或直接引用自身)会导致无限递归。WeakMap 可以优雅地解决这个问题:
解决方案原理
跟踪已拷贝对象:使用 WeakMap 记录原始对象和其拷贝的对应关系
遇到已拷贝对象直接返回:避免无限递归
自动内存管理:WeakMap 的弱引用特性不会阻止原始对象被垃圾回收