JS对象循环引用

1,296 阅读3分钟

从鸡&蛋问题聊起

鸡生蛋,蛋生鸡。假如鸡可以一直长生不死,蛋都能孵出母鸡,一直如此循环往复。理论上只需一只鸡,就能得到无尽的鸡和蛋。

那么这个现象,如果用JS来表达,是什么样呢,如下图:

  1. 我们先定义chickenegg对象;

  2. chicken生下一个egg

  3. egg孵出一只chicken

    image.png

最终我们得到了无穷无尽的鸡和蛋。

为什么要用循环引用

最近在做一个需求,对一个树状结构的数据进行搜索,结果平铺展示。数据的每一个节点状态都能改变,并且这个节点的改变会影响所有的子节点和父节点状态。

数据结构大概如下, 层级不确定:

const tree = [{
    id: '0',
    children: [
        {id: '00'},
        {
            id: '01',
            children: [
                {id: '011'},
                ...
            ]
        },
        {id: '02'},
        ...
    ]
}]

如果按以往的方法做:

  1. 将树状数据拍平过程中,添加父级id
  2. 某一节点状态改变,修改子节点状态;
  3. 遍历整棵树,根据父级id找到所有父节点,循环调用方法从内到外一层层更改状态;

明显可以发现,子节点是通过children字段挂载在当前节点上的,状态容易更改;父节点的话,则要向上查找,过程麻烦的多。

那么,如果我们知道当前节点的父节点的话,是不是容易很多。所以,在数据拍平过程中,给每个节点,都添加一个parent属性

const treeList = [];
const formatter = (tree, parent=null) => {
    tree.forEach(child => {
        child.parent = parent;
        treeList.push(child);
        if (child.children) {
            formatter(child.children, child);
        }
    });
};
formatter(tree);

那么,这样的话,当我们修改了某一个节点的状态,我们可以像获取子节点一样获取父节点,并且通过parent属性可以一直向上查找出所有的父节点,节省了大量的循环遍历过程。

对象的循环引用会有什么问题

  1. 对象循环引用会不会带来内存泄漏问题?

如果是采用引用计数法进行垃圾回收,确实会。但是现在标记清除法更加流行,这种垃圾回收机制下,不会造成内存泄漏问题。

  1. JSON.stringify()方法报错 在对一个循环引用的对象使用JSON.stringify()方法时会报错,这个确实存在。

image.png

如何避免JSON.stringify()方法报错问题

参考MDN对JSON.stringify的描述:

image.png

  1. 可以给对象添加一个toJSON方法;

    image.png

但此方法只能解决不报错问题,很难得到我们想要的结果。

  1. 使用symbol作为属性键

从描述可以看到,JSON.stringify()方法会自动忽略对象以symbol作为属性键的属性。所以,如果我们把循环引用的对象,挂载在以symbol作为属性键的属性上,不就OK了。

image.png

  1. 还可以使用这个JSON库来去除循环引用

代码挺简单的,只有几十行,主要思想是循环遍历,利用WeakMap来存储每一个对象,记录下对象访问路径的字符串;发现相同的对象,直接替换成访问路径字符串,从而破坏循环引用链路。另外,还提供了恢复的方法。

大家有兴趣可以自己去看看。

tips

对象的循环引用其实在js中很常见,浏览器的event事件中就有它的身影,event.target.childrenNodeevent.target.parentNode就可以相互访问,但是JSON.stringify(event.target)并不会报错,而是返回{}

总结

对象的循环引用,在某些时候确实挺好用的,但是使用的时候还是要小心!