JavaScript 深度拷贝实现与对象的循环引用判断
·
JavaScript 深度拷贝实现
深度拷贝(Deep Copy)是 JavaScript 中常见的需求,以下是多种实现方法和完整解决方案:
1. JSON 方法(最简单但有局限性)
function deepCloneByJSON(obj) {
return JSON.parse(JSON.stringify(obj));
}
// 优点:简单快捷
// 缺点:
// 1. 无法拷贝函数、undefined、Symbol
// 2. 无法拷贝循环引用对象
// 3. 无法拷贝特殊对象(Date, RegExp, Map, Set等)
// 4. 会忽略原型链
// 示例
const obj = { a: 1, b: { c: 2 }, d: new Date(), e: function() {} };
const cloned = deepCloneByJSON(obj);
console.log(cloned.d); // 字符串,不是Date对象
console.log(cloned.e); // undefined
2. 基础递归实现
function deepClone(obj) {
// 处理原始类型和函数
if (obj === null || typeof obj !== 'object') {
return obj;
}
// 处理数组
if (Array.isArray(obj)) {
const arrCopy = [];
for (let i = 0; i < obj.length; i++) {
arrCopy[i] = deepClone(obj[i]);
}
return arrCopy;
}
// 处理对象
const objCopy = {};
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
objCopy[key] = deepClone(obj[key]);
}
}
return objCopy;
}
3. 完整版深度拷贝(处理各种类型)
function deepCloneComplete(obj, hash = new WeakMap()) {
// 处理基本类型和函数
if (obj === null || typeof obj !== 'object') {
return obj;
}
// 处理循环引用
if (hash.has(obj)) {
return hash.get(obj);
}
// 处理Date
if (obj instanceof Date) {
const copy = new Date(obj.getTime());
hash.set(obj, copy);
return copy;
}
// 处理正则表达式
if (obj instanceof RegExp) {
const copy = new RegExp(obj.source, obj.flags);
hash.set(obj, copy);
return copy;
}
// 处理Map
if (obj instanceof Map) {
const copy = new Map();
hash.set(obj, copy);
obj.forEach((value, key) => {
copy.set(deepCloneComplete(key, hash), deepCloneComplete(value, hash));
});
return copy;
}
// 处理Set
if (obj instanceof Set) {
const copy = new Set();
hash.set(obj, copy);
obj.forEach(value => {
copy.add(deepCloneComplete(value, hash));
});
return copy;
}
// 处理数组
if (Array.isArray(obj)) {
const copy = [];
hash.set(obj, copy);
for (let i = 0; i < obj.length; i++) {
copy[i] = deepCloneComplete(obj[i], hash);
}
return copy;
}
// 处理普通对象
if (obj instanceof Object) {
// 获取原对象的原型
const copy = Object.create(Object.getPrototypeOf(obj));
hash.set(obj, copy);
// 拷贝所有属性(包括Symbol)
const allKeys = [
...Object.getOwnPropertyNames(obj),
...Object.getOwnPropertySymbols(obj)
];
for (const key of allKeys) {
const descriptor = Object.getOwnPropertyDescriptor(obj, key);
if (descriptor) {
if (descriptor.get || descriptor.set) {
// 处理访问器属性
Object.defineProperty(copy, key, descriptor);
} else {
// 处理数据属性
copy[key] = deepCloneComplete(obj[key], hash);
}
}
}
return copy;
}
// 其他类型直接返回
return obj;
}
4. 性能优化版本
function deepCloneOptimized(obj, hash = new WeakMap()) {
// 基础类型直接返回
if (obj === null || typeof obj !== 'object') {
return obj;
}
// 循环引用检查
if (hash.has(obj)) {
return hash.get(obj);
}
// 根据构造函数类型创建对应实例
const Constructor = obj.constructor;
let copy;
switch (Constructor) {
case Date:
copy = new Date(obj.getTime());
break;
case RegExp:
copy = new Constructor(obj.source, obj.flags);
break;
case Map:
copy = new Map();
hash.set(obj, copy);
obj.forEach((value, key) => {
copy.set(
deepCloneOptimized(key, hash),
deepCloneOptimized(value, hash)
);
});
return copy;
case Set:
copy = new Set();
hash.set(obj, copy);
obj.forEach(value => {
copy.add(deepCloneOptimized(value, hash));
});
return copy;
case Array:
copy = [];
hash.set(obj, copy);
for (let i = 0; i < obj.length; i++) {
copy[i] = deepCloneOptimized(obj[i], hash);
}
return copy;
default:
// 普通对象或自定义类实例
copy = Object.create(Object.getPrototypeOf(obj));
hash.set(obj, copy);
}
// 快速拷贝(使用Reflect.ownKeys获取所有键)
const keys = Reflect.ownKeys(obj);
for (const key of keys) {
const descriptor = Object.getOwnPropertyDescriptor(obj, key);
if (descriptor && (descriptor.get || descriptor.set)) {
Object.defineProperty(copy, key, descriptor);
} else {
copy[key] = deepCloneOptimized(obj[key], hash);
}
}
return copy;
}
5. 使用 MessageChannel 实现深度拷贝
function deepCloneMessageChannel(obj) {
return new Promise((resolve) => {
const { port1, port2 } = new MessageChannel();
port2.onmessage = (event) => resolve(event.data);
port1.postMessage(obj);
});
}
// 使用示例
async function example() {
const obj = { a: 1, b: { c: 2 } };
const cloned = await deepCloneMessageChannel(obj);
console.log(cloned);
}
// 注意:这是异步的,且同样有一些限制
// 可以拷贝函数、循环引用,但不能拷贝DOM元素、函数上下文等
6. 结构化克隆 API(现代浏览器)
// 使用 structuredClone API(现代浏览器支持)
function deepCloneStructured(obj) {
// 检查浏览器是否支持
if (typeof structuredClone === 'function') {
return structuredClone(obj);
} else {
// 降级方案
console.warn('structuredClone not supported, using fallback');
return deepCloneOptimized(obj);
}
}
// 优点:
// 1. 浏览器原生支持,性能好
// 2. 支持更多类型(包括循环引用)
// 3. 标准API
7. 针对特定场景的优化版本
function deepCloneWithOptions(obj, options = {}) {
const {
hash = new WeakMap(),
preserveProto = true,
copyDescriptors = true,
ignoreSymbols = false,
maxDepth = 50
} = options;
// 深度限制
if (maxDepth <= 0) {
console.warn('Maximum depth reached');
return obj;
}
// 基础类型检查
if (obj === null || typeof obj !== 'object') {
return obj;
}
// 循环引用检查
if (hash.has(obj)) {
return hash.get(obj);
}
// 特殊类型处理
const Constructor = obj.constructor;
let copy;
const newOptions = { ...options, maxDepth: maxDepth - 1 };
switch (Constructor) {
case Date:
copy = new Date(obj.getTime());
break;
case RegExp:
copy = new RegExp(obj.source, obj.flags);
break;
case Map:
copy = new Map();
hash.set(obj, copy);
obj.forEach((value, key) => {
copy.set(
deepCloneWithOptions(key, newOptions),
deepCloneWithOptions(value, newOptions)
);
});
return copy;
case Set:
copy = new Set();
hash.set(obj, copy);
obj.forEach(value => {
copy.add(deepCloneWithOptions(value, newOptions));
});
return copy;
case Array:
copy = [];
hash.set(obj, copy);
for (let i = 0; i < obj.length; i++) {
copy[i] = deepCloneWithOptions(obj[i], newOptions);
}
return copy;
default:
// 创建新对象,保留原型链
copy = preserveProto
? Object.create(Object.getPrototypeOf(obj))
: {};
hash.set(obj, copy);
}
// 获取所有属性键
let keys;
if (ignoreSymbols) {
keys = Object.getOwnPropertyNames(obj);
} else {
keys = [
...Object.getOwnPropertyNames(obj),
...Object.getOwnPropertySymbols(obj)
];
}
// 拷贝属性
for (const key of keys) {
if (copyDescriptors) {
const descriptor = Object.getOwnPropertyDescriptor(obj, key);
if (descriptor) {
if (descriptor.get || descriptor.set) {
Object.defineProperty(copy, key, descriptor);
} else if ('value' in descriptor) {
descriptor.value = deepCloneWithOptions(descriptor.value, newOptions);
Object.defineProperty(copy, key, descriptor);
}
}
} else {
copy[key] = deepCloneWithOptions(obj[key], newOptions);
}
}
return copy;
}
8. 使用示例
// 复杂对象示例
const complexObj = {
number: 123,
string: 'hello',
boolean: true,
null: null,
undefined: undefined,
array: [1, 2, { nested: 'value' }],
date: new Date(),
regexp: /test/gi,
map: new Map([['key', 'value']]),
set: new Set([1, 2, 3]),
symbol: Symbol('test'),
function: function() { console.log('hello'); },
// 循环引用
circular: null
};
complexObj.circular = complexObj;
complexObj.nested = { parent: complexObj };
// 使用深度拷贝
const cloned = deepCloneOptimized(complexObj);
// 验证
console.log(cloned !== complexObj); // true
console.log(cloned.array !== complexObj.array); // true
console.log(cloned.date.getTime() === complexObj.date.getTime()); // true
console.log(cloned.circular === cloned); // true(保持了循环引用结构)
9. 性能测试对比
function benchmark() {
const obj = createComplexObject(1000); // 创建复杂对象
console.time('JSON方法');
const jsonCopy = JSON.parse(JSON.stringify(obj));
console.timeEnd('JSON方法');
console.time('递归方法');
const recursiveCopy = deepCloneOptimized(obj);
console.timeEnd('递归方法');
// 验证正确性
console.log('JSON方法有效:', jsonCopy.array ? '是' : '否'); // 可能丢失数据
console.log('递归方法有效:', recursiveCopy.array ? '是' : '否');
}
function createComplexObject(depth) {
const obj = {
array: new Array(100).fill(0).map((_, i) => i),
nested: {},
date: new Date(),
map: new Map(),
set: new Set()
};
if (depth > 0) {
obj.nested = createComplexObject(depth - 1);
}
return obj;
}
选择建议
- 简单场景:使用
JSON.parse(JSON.stringify(obj)) - 现代浏览器:使用
structuredClone() - 复杂场景:使用
deepCloneOptimized或deepCloneComplete - 需要异步:考虑
MessageChannel方法 - 性能敏感:使用针对性的优化版本
注意事项
- 函数拷贝:函数通常不需要深度拷贝,因为它们很少被修改
- 原型链:根据需求决定是否保留原型链
- 不可枚举属性:考虑是否需要拷贝不可枚举属性
- 性能:深度拷贝大型对象可能消耗较多内存和时间
- 特殊对象:如 Buffer、ArrayBuffer 等可能需要特殊处理
根据具体需求选择合适的深度拷贝实现方案。
在JavaScript深度copy对象很多使用JSON.stringify 方法,当出现循环引用的情况下回报错,以下是判断循环引用的方法
1. 使用 WeakSet/Set 跟踪已访问对象
function hasCircularReference(obj) {
const seen = new WeakSet();
function detect(obj) {
if (obj && typeof obj === 'object') {
if (seen.has(obj)) {
return true;
}
seen.add(obj);
for (let key in obj) {
if (obj.hasOwnProperty(key) && detect(obj[key])) {
return true;
}
}
seen.delete(obj); // 注意:对于树形遍历需要删除
}
return false;
}
return detect(obj);
}
// 示例
const obj = { a: 1 };
obj.self = obj; // 循环引用
console.log(hasCircularReference(obj)); // true
2. 优化的版本(更准确)
function hasCyclicReferences(obj) {
const seen = new WeakSet();
function detect(obj, path = []) {
if (obj && typeof obj === 'object') {
if (seen.has(obj)) {
return true;
}
seen.add(obj);
const keys = Object.keys(obj);
for (let key of keys) {
const value = obj[key];
if (value && typeof value === 'object') {
if (detect(value, [...path, key])) {
return true;
}
}
}
}
return false;
}
return detect(obj);
}
3. 使用 JSON.stringify 的 try-catch 方法
function hasCircularJSON(obj) {
try {
JSON.stringify(obj);
return false;
} catch (error) {
// TypeError: Converting circular structure to JSON
return error.message.includes('circular');
}
}
// 注意:这种方法只能检测到最外层的循环引用
4. 完整功能检测函数(支持详细报告)
function findCircularReferences(obj) {
const seen = new WeakMap(); // 存储对象和路径
const circularPaths = [];
function traverse(obj, path = []) {
if (obj && typeof obj === 'object') {
if (seen.has(obj)) {
circularPaths.push({
object: obj,
path: path.join('.'),
firstSeenAt: seen.get(obj)
});
return true;
}
seen.set(obj, path.join('.') || 'root');
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
const value = obj[key];
if (value && typeof value === 'object') {
traverse(value, [...path, key]);
}
}
}
seen.delete(obj);
}
return false;
}
traverse(obj);
return {
hasCircular: circularPaths.length > 0,
circularPaths: circularPaths
};
}
// 使用示例
const obj = {
a: 1,
b: { c: 2 }
};
obj.b.d = obj.b; // 循环引用
const result = findCircularReferences(obj);
console.log(result.hasCircular); // true
console.log(result.circularPaths);
5. 处理特殊情况的增强版
function detectCircular(obj, options = {}) {
const {
maxDepth = 50,
ignoreBuiltIn = true,
checkArrays = true,
checkMapsAndSets = true
} = options;
const seen = new WeakSet();
let depth = 0;
function check(obj) {
if (depth > maxDepth) {
throw new Error('Maximum depth exceeded');
}
if (!obj || typeof obj !== 'object') {
return false;
}
// 忽略内置对象(可选)
if (ignoreBuiltIn) {
const constructorName = obj.constructor.name;
if (['Date', 'RegExp', 'Promise', 'Function'].includes(constructorName)) {
return false;
}
}
if (seen.has(obj)) {
return true;
}
seen.add(obj);
depth++;
try {
// 检查普通对象
if (Array.isArray(obj) && checkArrays) {
for (let item of obj) {
if (check(item)) return true;
}
}
// 检查Map
else if (obj instanceof Map && checkMapsAndSets) {
for (let [key, value] of obj) {
if (check(key) || check(value)) return true;
}
}
// 检查Set
else if (obj instanceof Set && checkMapsAndSets) {
for (let item of obj) {
if (check(item)) return true;
}
}
// 检查普通对象
else if (typeof obj === 'object') {
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
if (check(obj[key])) return true;
}
}
}
} finally {
depth--;
seen.delete(obj);
}
return false;
}
return check(obj);
}
6. 性能优化的版本(避免递归栈溢出)
function hasCircularBFS(obj) {
if (!obj || typeof obj !== 'object') return false;
const seen = new WeakSet();
const queue = [{ obj, path: [] }];
while (queue.length > 0) {
const { obj: current, path } = queue.shift();
if (seen.has(current)) {
return true;
}
seen.add(current);
if (Array.isArray(current)) {
for (let i = 0; i < current.length; i++) {
const item = current[i];
if (item && typeof item === 'object') {
queue.push({ obj: item, path: [...path, i] });
}
}
} else {
for (let key in current) {
if (current.hasOwnProperty(key)) {
const value = current[key];
if (value && typeof value === 'object') {
queue.push({ obj: value, path: [...path, key] });
}
}
}
}
}
return false;
}
使用示例
// 创建循环引用
const circularObj = { name: 'John' };
circularObj.self = circularObj;
const nestedCircular = {
a: { b: {} }
};
nestedCircular.a.b.c = nestedCircular.a;
// 检测
console.log(hasCircularReference(circularObj)); // true
console.log(hasCircularReference(nestedCircular)); // true
// 正常对象
const normalObj = { a: 1, b: { c: 2 } };
console.log(hasCircularReference(normalObj)); // false
注意事项
- WeakSet vs Set: 使用
WeakSet不会阻止垃圾回收,更适合这种场景 - 性能考虑: 深度嵌套或大型对象可能需要性能优化
- 边缘情况: 处理数组、Map、Set等特殊对象
- 最大深度: 避免无限递归,可以设置最大深度限制
- 内存使用: 跟踪所有访问过的对象可能会消耗内存
推荐使用第一种或第二种方法,它们相对简单且有效。如果需要更详细的信息(如循环路径),可以使用第四种方法。
更多推荐


所有评论(0)