关于对象的循环引用以及消除(恢复)循环引用的方法

5,203 阅读4分钟

什么是对象的循环引用?

  • 本质为:堆对堆的引用形成闭环造成了循环引用
    • 关于堆对堆的引用形成闭环(to be continue...)Go➡️
  • 表现方式大致分为两种:
  1. 自身引用 -- 对象的某个属性引用了对象自身
const obj1 = {
    a: 1,
}
obj1.b = obj;  // obj1的属性b引用了obj自己
console.log(obj);
/**
 * {
 *   a: 1,
 *   b: {
 *     a: 1,
 *     b: {
 *       a: 1,
 *       b: ...
 *     }
 *   }
 * }
 */
  1. 互相引用 -- 两个对象的属性间互相引用对方对象
const obj1 = {
  name: 'obj1',
}

const obj2 = {
  name: 'obj2',
}

// obj1 和 obj2 的val属性互相引用了对方
obj1.val = obj2;
obj2.val = obj1;
console.log(obj1);
/**
 * {
 *   name: 'obj1',
 *   val: {
 *     name: 'obj2',
 *     val: {
 *       name: 'obj1',
 *       val: { ... }
 *     }
 *   }
 * }
 */
  • 关于同级引用的声明
    • 同级引用
const obj = {
    a: 1,
    b: 2,
    c: {
    
    }
}
obj.c.d = obj.a;
  • 同级引用造成了对象深拷贝时引用丢失的问题(不作详述)
  • 一些博客将同级引用列入循环引用当中,但我认为它不符合 堆对堆的引用形成闭环的定义(尽管这句定义仅仅是个人理解!),并且在序列化中并不会报循环的错误,固没有在此列为 循环引用的方式 的子项。【欢迎纠正】

循环引用会造成什么问题?

  1. JSON数据的序列化错误
const a = { name: 'a' }
const b = { name: 'b' }
a.val = b;
b.val = a;
JSON.stringigy(obj1);  // error

  1. 对象的深拷贝不能正确处理循环引用 / 递归爆栈
const a = { name: 'a' }
const b = { name: 'b' }
a.val = b;
b.val = a;
const c = _deepCopy(obj1);
console.log(c);
/**
  * {
  *   ...someProperty,
  *   [Circular]
  * }
  */

Uncaught RangeError: Maximum call stack size exceeded

  1. 内存泄漏(基于引用计数算法的gc)
    • 浏览器已经舍弃引用计数算法,统一采用标记清除法来进行垃圾回收。
    • 关于垃圾回收机制:js的垃圾回收机制(to be continue...)Go➡️

如何解决(消除)循环引用?

思路:将引用闭环打破

  • 方法一:利用WeakMap结构将循环引用部分的强引用变为弱引用(可忽略此部分)
const obj1 = new WeakMap();
const obj2 = new WeakMap();
obj1.name = 'obj1';
obj2.name = 'obj2';

val1 = new String('val');
val2 = new String('val');
obj1.set(val1, obj2);  // 弱引用,引用计数为0
obj2.set(val2, obj1);  // 弱引用,引用计数为0
  1. 此方法解决了基于引用计数算法的垃圾回收机制所引起的内存泄漏的问题
  2. 由于WeakMap结构不存在iterator,所以WeakMap不可以遍历。
  3. JSON.stringigy()会忽略WeakMap结构的弱引用部分
const obj = new WeakMap();
const sayHello = new String('sayHello');
obj.set(sayHello, 'a ou');
obj.sayHaHa = 'haha';

console.log(obj.get(sayHello));  // a ou
console.log(obj.sayHaHa);  // haha
console.log(JSON.stringify(obj));  // { "sayHaHa": "haha" }
  • 方法二:判断对象的值是否引用了任意父级,强制修改
    • 详见对象的深拷贝(to be continue...)Go➡️
  • 方法三:Douglas Crockford撰写的JSON-js中提供的decycle方法处理了循环引用 查看github源码

对cycle.js中decycle方法的解读

  • 缩减后的decycle函数
if (typeof JSON.decycle !== "function") {
  JSON.decycle = function decycle(object, replacer) {
    "use strict";
    
    var objects = new WeakMap();

    return (function derez(value, path) {
      var old_path;
      var nu;

        if (replacer !== undefined) {
          value = replacer(value);
        }

        if (
          typeof value === "object"
          && value !== null
          && !(value instanceof Boolean)
          && !(value instanceof Date)
          && !(value instanceof Number)
          && !(value instanceof RegExp)
          && !(value instanceof String)
        ) {
          old_path = objects.get(value);
          if (old_path !== undefined) {
            return {$ref: old_path};
          }

          objects.set(value, path);

          if (Array.isArray(value)) {
            nu = [];
            value.forEach(function (element, i) {
              nu[i] = derez(element, path + "[" + i + "]");
            });
          } else {
            nu = {};
            Object.keys(value).forEach(function (name) {
              nu[name] = derez(
                value[name],
                path + "[" + JSON.stringify(name) + "]"
              );
            });
          }
          return nu;
        }
        return value;
      }(object, "$"));
    };
}

  • 解读:

核心思路:利用WeakMap存储遍历的对象,通过循环识别重复遍历的对象即发生了循环引用。使用标记的路径来替换发生循环引用键的值,打破引用的闭环。单个$为顶层对象,[]中为路径。

  1. JSON对象上定义了decycle方法,返回一个立即执行函数的处理结果
    • decyle
      • 参数object为处理对象
      • 参数replacer为一个函数,对象的每个成员都将调用replacer对其进行处理
    • derez
      • 参数value为处理对象
      • 参数path为循环引用标记的路径
if (typeof JSON.decycle !== "function") {
    JSON.decycle = function decycle(object, replacer) {
        var objects = new WeakMap();  // WeakMap结构存储遍历的到的对象
        return (function derez(value, path) {
            var old_path;   // 上一级标记的路径
            var nu;         // 容器 -- 新的数组 或 对象
    
            if (replacer !== undefined) {
              value = replacer(value);  // 处理传入的对象
            }
            
            // ...
        }(object, '&')
    }
}
  1. 判断参数value是否为对象(排除不存在循环问题的对象例如Date, 对象型字符String等)
    • 否: 返回自身
    • 是: 下一步处理
if (  // 判断是否为对象,排除不会造成循环问题的类型
    typeof value === "object"
    && value !== null
    && !(value instanceof Boolean)
    && !(value instanceof Date)
    && !(value instanceof Number)
    && !(value instanceof RegExp)
    && !(value instanceof String)
  ) {
    // 下一步处理
  }
  return value;  // 返回原本的值
  1. 判断当前遍历对象是否曾经遍历过
old_path = objects.get(value);
if (old_path !== undefined) {
    return {$ref: old_path};  // 当前对象遍历过 => 此处发生了循环引用;返回标记路径,打破循环引用
}

objects.set(value, path);  // 记录遍历过的对象
// ...
  1. 递归执行derez,对value结构进行处理
if (Array.isArray(value)) {  // 处理数组
  nu = [];
  value.forEach(function (element, i) {
      nu[i] = derez(element, path + "[" + i + "]");  // 更新标记路径
  });
} else {  // 处理对象
  nu = {};
  Object.keys(value).forEach(function (name) {
      nu[name] = derez(
          value[name],
          path + "[" + JSON.stringify(name) + "]"  // 更新标记路径
      );
  });
}
return nu;  // 返回处理结果

  • 运行示例
const obj1 = { name: 'obj1' }
const obj2 = { name: 'obj2' }

obj2.val = obj1;
obj1.val = obj2;

console.log(JSON.decycle(obj1));
// {
//   name: 'obj1',
//   val: {
//     name: 'obj2',
//     val: { '$ref': '$' }  // 标记的路径
//   }
// }
const obj1 = { name: 'obj1', children:[{}] }
const obj2 = { name: 'obj2', children:[{}] }

obj1.children[0].val = obj2.children[0];
obj2.children[0].val = obj1.children[0]

console.log(JSON.decycle(obj1));
// {
//   name: 'obj1',
//   children: [
//     {
//       val: {
//         val: { $ref: `$["children"][0]` }  // 标记的路径 $顶层对象
//       }
//     }
//   ]
// }

恢复循环引用

  • 采用cycle.js中的retrocycle方法
  • 依靠正则解读$ref路径的值为path
  • 用eval(path)进行恢复







关于本文

  • 文章非摘录,经浏览文档,以自己的理解进行,代码测试,手打书写。
  • 用作记录自己曾经学习、思考过的问题的一种笔记。
  • 用作前端技术交流分享。
  • 阅读本文时欢迎随时质疑本文的准确性,将错误的地方告诉我。本人会积极修改,避免文章对读者的误导。

关于我

  • 是一只有梦想的肥柴。
  • 觉得算法、数据结构、函数式编程、js底层原理等十分有趣的小前端。
  • 志同道合的朋友请关注我,一起交流技术,在前端之路上共同成长。
  • 如对本人有任何意见建议尽管告诉我哦~ 初为肥柴,请多多关照~
  • 前端路漫漫,技术学不完。今天也是美(diao)好(fa)的一天( 跪了...orz



参考文献:

Douglas Crockford.JSON-js/cycle.js[EB/OL].github.com/douglascroc…, 2018-5-15.