什么是对象的循环引用?
- 本质为:堆对堆的引用形成闭环造成了循环引用
- 关于
堆对堆的引用形成闭环(to be continue...)Go➡️
- 关于
- 表现方式大致分为两种:
- 自身引用 -- 对象的某个属性引用了对象自身
const obj1 = {
a: 1,
}
obj1.b = obj; // obj1的属性b引用了obj自己
console.log(obj);
/**
* {
* a: 1,
* b: {
* a: 1,
* b: {
* a: 1,
* b: ...
* }
* }
* }
*/
- 互相引用 -- 两个对象的属性间互相引用对方对象
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;
- 同级引用造成了对象深拷贝时引用丢失的问题(不作详述)
- 一些博客将同级引用列入循环引用当中,但我认为它不符合
堆对堆的引用形成闭环的定义(尽管这句定义仅仅是个人理解!),并且在序列化中并不会报循环的错误,固没有在此列为 循环引用的方式 的子项。【欢迎纠正】
循环引用会造成什么问题?
- JSON数据的序列化错误
const a = { name: 'a' }
const b = { name: 'b' }
a.val = b;
b.val = a;
JSON.stringigy(obj1); // error

- 对象的深拷贝不能正确处理循环引用 / 递归爆栈
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
内存泄漏(基于引用计数算法的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
- 此方法解决了
基于引用计数算法的垃圾回收机制所引起的内存泄漏的问题 - 由于WeakMap结构不存在iterator,所以WeakMap不可以遍历。
- 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存储遍历的对象,通过循环识别重复遍历的对象即发生了循环引用。使用标记的路径来替换发生循环引用键的值,打破引用的闭环。单个$为顶层对象,[]中为路径。
- JSON对象上定义了decycle方法,返回一个立即执行函数的处理结果
- decyle
- 参数
object为处理对象 - 参数
replacer为一个函数,对象的每个成员都将调用replacer对其进行处理
- 参数
- derez
- 参数
value为处理对象 - 参数
path为循环引用标记的路径
- 参数
- decyle
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, '&')
}
}
- 判断参数
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; // 返回原本的值
- 判断当前遍历对象是否曾经遍历过
old_path = objects.get(value);
if (old_path !== undefined) {
return {$ref: old_path}; // 当前对象遍历过 => 此处发生了循环引用;返回标记路径,打破循环引用
}
objects.set(value, path); // 记录遍历过的对象
// ...
- 递归执行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
