Skip to content

对象的循环引用


在 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;
}

原理说明

  1. 核心逻辑
    使用 Set 存储已访问过的对象(Set 可以直接存储对象引用并判断是否存在)。遍历对象时,若当前对象已在 Set 中,则说明存在循环引用。

  2. 递归遍历
    对对象的每个属性值递归执行检测,确保所有嵌套对象都被检查。

  3. 边界处理

    • 非对象类型(如基本类型、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

注意事项

  • 性能:对于深层嵌套的大型对象,递归可能存在性能问题,可考虑改用迭代方式实现。
  • 特殊对象:该方法对 MapSet 等内置对象可能需要额外处理(需遍历其元素)。
  • 循环引用的影响:存在循环引用的对象无法被 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

// 这就是一个相同的地址

浅拷贝只处理对象的第一层属性,嵌套对象仍共享引用。

  1. 手动遍历赋值
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' } } false
  1. Object.assign()
javascript
const obj = { a: 1, b: { c: 2 } };
const shallowCopy = Object.assign({}, obj);

shallowCopy.b.c = 3;
console.log(obj.b.c); // 3(原对象受影响)
  1. 数组的 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(原数组受影响)

深拷贝会递归复制所有层级,确保拷贝后的数据完全独立。

  1. 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(原对象不受影响)
  1. 递归实现深拷贝(完整版)
特性浅拷贝深拷贝
复制层级仅复制第一层属性递归复制所有层级(包括嵌套对象)
引用类型处理嵌套对象共享引用嵌套对象完全独立
对原对象的影响修改嵌套对象会影响原对象修改拷贝后的数据不影响原对象
适用场景简单结构、无需独立嵌套对象复杂结构、需要完全独立的数据
性能效率高(复制层级少)效率较低(递归处理所有层级)
  • 浅拷贝适合简单对象,实现简单但无法隔离嵌套引用类型的修改。
  • 深拷贝适合复杂对象(含嵌套结构、循环引用等),确保数据完全独立,但实现较复杂且性能开销更大。
  • 实际开发中,可根据数据复杂度选择合适的拷贝方式:简单场景用 Object.assign() 或扩展运算符,复杂场景用递归深拷贝函数。