在JavaScript开发中,对象拷贝是常见但易错的操作。本文基于原生JavaScript实现支持循环引用、保留属性描述符、处理所有内置对象的生产级深拷贝函数,解决了Lodash等库的局限性。通过详细技术分析和150行完整代码实现,掌握可靠的深拷贝解决方案。
当JavaScript对象包含嵌套引用时,浅拷贝仅复制第一层引用,深层次对象仍与原对象共享内存地址。这会导致数据污染风险:修改拷贝对象时可能意外修改原始数据。
常见深拷贝方案对比:
方法 | 循环引用 | 函数 | 属性描述符 | Symbol | 特殊对象 |
---|---|---|---|---|---|
JSON.parse(JSON.stringify()) |
❌ | ❌ | ❌ | ❌ | 部分支持 |
Lodash _.cloneDeep |
✅ | ✅ | ❌ | ❌ | ✅ |
本文实现方案 | ✅ | ✅ | ✅ | ✅ | ✅ |
循环引用是深拷贝的最大挑战,当对象A引用B而B又引用A时,常规递归会导致栈溢出。
解决方案核心:
function deepClone(obj, cache = new WeakMap()) {
if (cache.has(obj)) return cache.get(obj); // 存在则返回缓存
const copy = ...; // 创建空副本
cache.set(obj, copy); // 立即缓存
// 递归处理属性
return copy;
}
WeakMap优势:
if (obj instanceof Date) {
return new Date(obj.getTime());
}
关键点:
getTime()
获取时间戳而非valueOf()
toISOString()
解析导致的性能损失if (obj instanceof RegExp) {
const copy = new RegExp(obj.source, obj.flags);
copy.lastIndex = obj.lastIndex; // 复制匹配状态
return copy;
}
状态保留:
source
: 正则表达式文本flags
: 匹配标志(g/i/m/y/u)lastIndex
: 上次匹配位置// Map处理
if (obj instanceof Map) {
const copy = new Map();
obj.forEach((v, k) =>
copy.set(deepClone(k, cache), deepClone(v, cache))
);
}
// Set处理
if (obj instanceof Set) {
const copy = new Set();
obj.forEach(v => copy.add(deepClone(v, cache)));
}
注意事项:
函数处理原则:
if (typeof obj === 'function') {
const copy = function(...args) {
return obj.apply(this, args);
};
// 复制函数属性
Object.getOwnPropertyNames(obj).forEach(prop => {
copy[prop] = deepClone(obj[prop], cache);
});
return copy;
}
限制:
Symbol处理:
if (typeof obj === 'symbol') {
return Symbol(obj.description); // 仅能复制描述
}
Symbol.for()
需复用全局注册表普通深拷贝常丢失的关键元信息:
完整复制实现:
const descs = Object.getOwnPropertyDescriptors(obj);
const clonedDescs = {};
Object.entries(descs).forEach(([key, desc]) => {
if ('value' in desc) {
clonedDescs[key] = {
...desc,
value: deepClone(desc.value, cache)
};
} else { // 访问器属性
clonedDescs[key] = desc; // 保持getter/setter
}
});
Object.defineProperties(copy, clonedDescs);
特殊案例处理:
Object.freeze
状态Object.seal
状态Object.create(Object.getPrototypeOf(obj))
export function deepClone(obj, cache = new WeakMap()) {
// 基础类型直接返回
if (obj === null || (typeof obj !== 'function' && typeof obj !== 'object')) {
return obj
}
// 处理循环引用
if (cache.has(obj)) return cache.get(obj)
const handlerMap = {
Date: cloneDate,
RegExp: cloneRegExp,
Map: cloneMap,
Set: cloneSet,
Function: cloneFunction,
}
// 获取对象的构造函数名称
const objType = Object.prototype.toString.call(obj).slice(8, -1)
if (handlerMap[objType]) {
return handlerMap[objType](obj, cache)
}
// 处理数组/对象
const copy = Array.isArray(obj)
? []
: Object.create(Object.getPrototypeOf(obj)) // 保留原型链
cache.set(obj, copy)
// 处理属性描述符
const descriptors = Object.getOwnPropertyDescriptors(obj)
// 批量处理属性描述符
Object.defineProperties(copy, processDescriptors(descriptors, cache))
// 单独处理Symbol键
Reflect.ownKeys(obj).forEach((key) => {
if (typeof key === 'symbol') {
copy[key] = deepClone(obj[key], cache)
}
})
return copy
}
function processDescriptors(descriptors, cache) {
const result = {}
Object.entries(descriptors).forEach(([key, desc]) => {
// 跳过不可克隆的内置属性
const skipList = [
'constructor',
'prototype',
'length',
'name',
'arguments',
'caller',
'callee',
]
if (skipList.includes(key)) return
// 处理访问器属性
if ('get' in desc || 'set' in desc) {
result[key] = {
get: deepClone(desc.get, cache),
set: deepClone(desc.set, cache),
enumerable: desc.enumerable,
configurable: desc.configurable,
}
}
// 处理数据属性
else if ('value' in desc) {
result[key] = {
value: deepClone(desc.value, cache),
enumerable: desc.enumerable,
configurable: desc.configurable,
writable: desc.writable,
}
}
})
return result
}
function cloneFunction(func, cache) {
if (cache.has(func)) return cache.get(func)
// 创建函数副本
const copy = function (...args) {
return func.apply(this, args)
}
// 复制原型链
Object.setPrototypeOf(copy, Object.getPrototypeOf(func))
// 安全复制函数属性
const descriptors = Object.getOwnPropertyDescriptors(func)
Object.defineProperties(copy, processDescriptors(descriptors, cache))
cache.set(func, copy)
return copy
}
function cloneDate(date, cache) {
const copy = new Date(date)
cache.set(date, copy)
return copy
}
function cloneRegExp(regex, cache) {
const copy = new RegExp(regex.source, regex.flags)
copy.lastIndex = regex.lastIndex
cache.set(regex, copy)
return copy
}
function cloneMap(map, cache) {
const copy = new Map()
cache.set(map, copy)
map.forEach((value, key) => {
copy.set(deepClone(key, cache), deepClone(value, cache))
})
return copy
}
function cloneSet(set, cache) {
const copy = new Set()
cache.set(set, copy)
set.forEach((value) => copy.add(deepClone(value, cache)))
return copy
}
使用Vitest进行验证:
import { describe, expect, it } from 'vitest'
import { deepClone } from './deepCopy.mjs'
describe('deepClone', () => {
it('克隆基本类型', () => {
expect(deepClone(42)).toBe(42)
expect(deepClone('text')).toBe('text')
expect(deepClone(true)).toBe(true)
expect(deepClone(null)).toBe(null)
})
it('处理函数', () => {
const fn = () => {}
fn.deps = [1, 2, 3, 4, 5]
const cloned = deepClone(fn)
expect(cloned).toBeInstanceOf(Function)
expect(cloned).not.toBe(fn)
expect(cloned.deps).not.toBe(fn.deps)
expect(cloned.deps).toEqual(fn.deps)
})
it('处理循环引用', () => {
const obj = { self: null }
obj.self = obj
const cloned = deepClone(obj)
expect(cloned).not.toBe(obj)
expect(cloned.self).toBe(cloned)
})
it('复制Date对象', () => {
const date = new Date('2023-01-01')
const cloned = deepClone(date)
expect(cloned.getTime()).toBe(date.getTime())
expect(cloned).not.toBe(date)
})
it('复制RegExp对象', () => {
const regex = /test/g
regex.lastIndex = 2
const cloned = deepClone(regex)
expect(cloned.source).toBe('test')
expect(cloned.flags).toBe('g')
expect(cloned.lastIndex).toBe(2)
})
it('复制Map对象', () => {
const map = new Map()
const key = {}
map.set(key, [1, 2, 3])
const cloned = deepClone(map)
expect(cloned).toEqual(map)
expect(cloned.entries().next().value[0]).not.toBe(key)
expect(cloned.entries().next().value[1]).not.toBe(map.get(key))
expect(cloned.entries().next().value[1]).toEqual(map.get(key))
})
it('保留属性描述符', () => {
const obj = {}
Object.defineProperty(obj, 'readonly', {
value: 123,
writable: false,
enumerable: false,
})
const cloned = deepClone(obj)
const desc = Object.getOwnPropertyDescriptor(cloned, 'readonly')
expect(desc.value).toBe(123)
expect(desc.writable).toBe(false)
expect(desc.enumerable).toBe(false)
})
it('处理访问器属性', () => {
const obj = {
get value() {
return this._v
},
set value(v) {
this._v = v
},
}
const cloned = deepClone(obj)
cloned.value = 10
expect(cloned.value).toBe(10)
expect('value' in Object.getPrototypeOf(cloned)).toBe(false)
})
it('处理Symbol属性', () => {
const symbol = Symbol('test')
const obj = { [symbol]: 'value' }
const cloned = deepClone(obj)
expect(cloned[symbol]).toBe('value')
})
it('克隆不可枚举属性', () => {
const obj = {}
Object.defineProperty(obj, 'hidden', {
value: 'secret',
enumerable: false,
})
const cloned = deepClone(obj)
expect(Object.keys(cloned)).toEqual([])
expect(Object.getOwnPropertyNames(cloned)).toEqual(['hidden'])
})
})
场景 | 推荐方案 | 原因 |
---|---|---|
简单对象拷贝 | JSON.parse/stringify | 性能最佳 |
性能敏感场景 | Lodash _.cloneDeep | 优化良好的递归实现 |
需要完整对象复制 | 本文实现方案 | 保留所有属性元信息 |
需要特殊对象处理 | 结构化克隆API | 原生支持更多类型 |
实现生产级深拷贝必须解决循环引用、特殊对象和属性完整性三大核心问题。本文方案通过WeakMap解决循环引用、完整复制属性描述符,并针对所有JavaScript内置对象提供专门处理逻辑。
最佳实践建议:
- 优先使用浏览器原生结构化克隆方案(如
postMessage
)- 状态管理库中优先考虑不可变数据结构
- 高频深拷贝场景建议添加缓存层优化
实现完全可靠的深拷贝是JavaScript高级开发的必备能力,掌握这些核心原理将帮助您在复杂场景下避免数据污染风险。