对象的循环引用
在 JavaScript 中,检测对象是否存在循环引用(即对象直接或间接引用自身)可以通过追踪已访问对象的方式实现。核心思路是:遍历对象时记录已访问过的对象,若再次遇到同一对象则说明存在循环引用。
方法及原理:递归遍历 + 已访问对象集合
javascript
/**
* 检测对象是否存在循环引用
* @param {Object} obj - 要检测的对象
* @param {Set} [visited] - 用于记录已访问对象的集合(内部递归使用)
* @returns {boolean} 是否存在循环引用
*/
function hasCycle(obj, visited = new Set()) {
// 若不是对象或为 null,直接返回 false(非对象不会有循环引用)
if (obj === null || typeof obj !== 'object') {
return false;
}
// 若当前对象已被访问过,说明存在循环引用
if (visited.has(obj)) {
return true;
}
// 将当前对象加入已访问集合
visited.add(obj);
// 递归遍历对象的所有属性
for (const key in obj) {
// 只遍历对象自身的可枚举属性
if (obj.hasOwnProperty(key)) {
// 递归检测属性值
if (hasCycle(obj[key], visited)) {
return true;
}
}
}
// 遍历完成后移除当前对象(不影响其他分支的检测)
visited.delete(obj);
return false;
}原理说明
核心逻辑:
使用Set存储已访问过的对象(Set可以直接存储对象引用并判断是否存在)。遍历对象时,若当前对象已在Set中,则说明存在循环引用。递归遍历:
对对象的每个属性值递归执行检测,确保所有嵌套对象都被检查。边界处理:
- 非对象类型(如基本类型、
null)不会产生循环引用,直接返回false。 - 使用
hasOwnProperty只遍历对象自身属性,避免遍历原型链上的属性。
- 非对象类型(如基本类型、
示例验证
javascript
// 无循环引用的对象
const obj1 = { a: 1, b: { c: 2 } };
console.log(hasCycle(obj1)); // false
// 有循环引用的对象(直接引用自身)
const obj2 = { x: 1 };
obj2.y = obj2;
console.log(hasCycle(obj2)); // true
// 有循环引用的对象(间接引用自身)
const obj3 = { a: {} };
obj3.b = { c: obj3 };
console.log(hasCycle(obj3)); // true注意事项
- 性能:对于深层嵌套的大型对象,递归可能存在性能问题,可考虑改用迭代方式实现。
- 特殊对象:该方法对
Map、Set等内置对象可能需要额外处理(需遍历其元素)。 - 循环引用的影响:存在循环引用的对象无法被
JSON.stringify序列化(会报错),也可能导致内存泄漏(需手动解除引用)。
通过这种方式,可以有效检测大多数常见场景下的循环引用问题。
深拷贝与浅拷贝
在 JavaScript 中,深拷贝和浅拷贝是针对引用类型数据(如对象、数组)的复制操作,核心区别在于是否复制对象的 “深层结构”:
- 浅拷贝:只复制对象的表层结构,对于嵌套的引用类型(如对象中的对象),仅复制其引用(内存地址),修改拷贝后的嵌套对象会==影响原对象==。
- 深拷贝:完全复制对象的所有层级结构,包括嵌套的引用类型,拷贝后的数据与原对象完全独立,修改==互不影响==。
javascript
const obj1 = {
a: 2,
b: 3,
c: {
name: "dano",
},
};
let obj2 = {}
obj2 = obj1;
console.log(obj2);
//{ a: 2, b: 3, c: { name: 'dano' } }
obj2.c.name = 'shit';
console.log(obj1,obj2,obj1===obj2)
//{ a: 2, b: 3, c: { name: 'shit' } } { a: 2, b: 3, c: { name: 'shit' } } true
// 这就是一个相同的地址浅拷贝只处理对象的第一层属性,嵌套对象仍共享引用。
- 手动遍历赋值
javascript
const obj = {
a: 2,
b: 3,
c: {
name: "2",
},
};
const obj2 = {};
for (key in obj) {
if (obj.hasOwnProperty(key)) {
obj2[key] = obj[key];
}
}
console.log(obj, obj2, obj === obj2);
// { a: 2, b: 3, c: { name: '2' } } { a: 2, b: 3, c: { name: '2' } } false
obj.a = "test"
console.log(obj, obj2, obj === obj2);
// { a: 'test', b: 3, c: { name: '2' } } { a: 2, b: 3, c: { name: '2' } } false
obj.c.name = "test2"
console.log(obj, obj2, obj === obj2);
// { a: 'test', b: 3, c: { name: 'test2' } } { a: 2, b: 3, c: { name: 'test2' } } falseObject.assign()
javascript
const obj = { a: 1, b: { c: 2 } };
const shallowCopy = Object.assign({}, obj);
shallowCopy.b.c = 3;
console.log(obj.b.c); // 3(原对象受影响)- 数组的
slice()、concat()或扩展运算符
javascript
const arr = [1, [2, 3]];
const shallowCopy = [...arr]; // 或 arr.slice()、[].concat(arr)
shallowCopy[1][0] = 4;
console.log(arr[1][0]); // 4(原数组受影响)深拷贝会递归复制所有层级,确保拷贝后的数据完全独立。
JSON.parse(JSON.stringify())(简单场景) 利用 JSON 序列化与反序列化实现深拷贝,缺点是无法处理函数、Symbol、循环引用等。
javascript
const obj = { a: 1, b: { c: 2 } };
const deepCopy = JSON.parse(JSON.stringify(obj));
deepCopy.b.c = 3;
console.log(obj.b.c); // 2(原对象不受影响)- 递归实现深拷贝(完整版)
| 特性 | 浅拷贝 | 深拷贝 |
|---|---|---|
| 复制层级 | 仅复制第一层属性 | 递归复制所有层级(包括嵌套对象) |
| 引用类型处理 | 嵌套对象共享引用 | 嵌套对象完全独立 |
| 对原对象的影响 | 修改嵌套对象会影响原对象 | 修改拷贝后的数据不影响原对象 |
| 适用场景 | 简单结构、无需独立嵌套对象 | 复杂结构、需要完全独立的数据 |
| 性能 | 效率高(复制层级少) | 效率较低(递归处理所有层级) |
- 浅拷贝适合简单对象,实现简单但无法隔离嵌套引用类型的修改。
- 深拷贝适合复杂对象(含嵌套结构、循环引用等),确保数据完全独立,但实现较复杂且性能开销更大。
- 实际开发中,可根据数据复杂度选择合适的拷贝方式:简单场景用
Object.assign()或扩展运算符,复杂场景用递归深拷贝函数。