在将带有循环引用的对象从服务器传递到客户端JavaScript时保持循环引用。

12 浏览
0 Comments

在将带有循环引用的对象从服务器传递到客户端JavaScript时保持循环引用。

我正在尝试将具有循环引用的对象从node.js服务器传递到客户端的javascript。

服务器端(node.js):

var object = { circular: object }
//....
app.get('/', function(req, res){    
    res.render('index.jade', {object: object});
});

客户端Jade/Javascript:

script var object = !{JSON.stringify(object)};

这里我得到了一个错误,即object包含循环引用。

有没有办法在客户端javascript中获取object带有

或者不带

循环引用?

0
0 Comments

问题的出现原因:在将带有循环引用的对象从服务器传递到客户端的JavaScript时,需要找到一种方法在保留循环引用的同时对对象图进行编码。

解决方法:使用"siberia"包来解决这个问题。首先,通过示例来解释算法。然后,通过对对象图进行编码来序列化对象,并使用整数标签替换值。将对象图中的每个对象的“冻结版本”作为具有相同键的对象,但将每个值替换为某个整数。这些整数是对象表或原子表中的索引。然后,反向过程是根据对象表和原子表将冻结的对象转换为原始对象。

文章如下:

我推荐使用我的"siberia"包,它可以在github上找到。

首先,我将通过示例来解释算法。这是我们的示例数据结构(Joe喜欢苹果和橙子,而Jane喜欢苹果和梨):

var Joe = { name: 'Joe' };
var Jane = { name: 'Jane' };
var Apple = { name: 'Apple' };
var Orange = { name: 'Orange' };
var Pear = { name: 'Pear' };
function addlike(person, fruit){
    person.likes = person.likes || [];
    fruit.likedBy = fruit.likedBy || [];
    person.likes.push(fruit);
    fruit.likedBy.push(person);
}
addlike(Joe, Apple); addlike(Joe, Orange);
addlike(Jane, Apple); addlike(Jane, Pear);
var myData = { people: [Joe, Jane], fruits: [Apple, Orange, Pear] };

这给我们带来了以下对象图(根对象`myData`是具有王冠的树,位于左侧中央):

![ObjectGraph_IMG](https://mathheadinclouds.github.io/img/ObjectGraph.png)

要进行序列化,我们必须找到一种方法以非循环的方式对对象图中的信息进行编码。通过将递归`discover`函数应用于根对象来确定对象图时(`discover`本质上是深度优先搜索,并添加了一个针对已见对象的停止条件),我们可以轻松地“计数”对象图的节点(对象),即用连续整数为它们标记,从零开始(零始终是根的标签)。然后,我们还会“计数”遇到的非对象(或“原子”);它们将获得负整数作为标签。在我们的示例中,所有原子都是字符串。(更一般的示例可能包含其他原子类型,如数字和“准原子对象”,例如正则表达式或日期对象(JavaScript内置对象有一种标准的字符串化方法))。例如,梨之所以获得数字8,仅仅是因为它是我们搜索算法发现的第8个对象。下面是添加了整数标签的对象图:

![ObjectGraphWithLabels_IMG](https://mathheadinclouds.github.io/img/ObjectGraphWithLabels.png)

有了整数标签,序列化就变得很容易。每个对象的“冻结版本”是一个具有相同键的对象,但是将每个值替换为某个整数。这些整数是对象表或原子表中的索引。

这是我们示例的对象表:

![the13objects_IMG](https://mathheadinclouds.github.io/img/the13objects.png)

和原子表:

![the5atoms_IMG](https://mathheadinclouds.github.io/img/the5atoms.png)

例如,梨对象的冻结版本(具有`.name = "Pear"`和`.likedBy` = `Jane`的数组)是对象`{name: -4, likedBy: 9}`,因为原子表的第4个插槽中是字符串`"Pear"`,并且对象表的第9个插槽中是包含Jane的数组。

这是一个稍微简化的序列化算法的代码(除了处理原子类型的部分,它将适用于我们的示例数据结构),共有32行代码:

function forestify_aka_decycle(root){
    var objects = [], inverseObjects = new WeakMap(), forest = [];
    var atomics = {}, atomicCounter = 0;
    function discover(obj){
        var currentIdx = objects.length;
        inverseObjects.set(obj, currentIdx);
        objects.push(obj);
        forest[currentIdx] = Array.isArray(obj) ? [] : {};
        Object.keys(obj).forEach(function(key){
            var val = obj[key], type = typeof val;
            if ((type==='object')&&(val!==null)){
                if (inverseObjects.has(val)){ // known object already has index
                    forest[currentIdx][key] = inverseObjects.get(val);
                } else {                      // unknown object, must recurse
                    forest[currentIdx][key] = discover(val);
                }
            } else {
                if (!(val in atomics)){
                    ++atomicCounter;                 // atoms table new entry
                    atomics[val] = atomicCounter;
                }
                forest[currentIdx][key] = -atomics[val];      // rhs negative
            }
        });
        return currentIdx;
    }
    discover(root);
    return {
        objectsTable: forest,
        atomsTable  : [null].concat(Object.keys(atomics))
    };
}

对象表是一个数组,在每个插槽中包含一个简单的键值对列表(是一个深度为1的树),因此被称为"forest"(森林)。

由于森林的树是冻结的,因此选择"siberia"作为算法的名称。

反向过程(`unforestify`,又名`retrocycle`)更加简单:首先,对于森林中的每棵树,生成一个空数组或空普通对象。然后,在对森林的树和树的键进行双重循环时,执行明显的赋值操作,右侧是从正在构建的解冻森林中的解冻树中获取的树,或从原子表中获取的原子。

function thawForest(forestified) {
    var thawedObjects = [];
    var objectsTable = forestified.objectsTable;
    var atomsTable = forestified.atomsTable;
    var i, entry, thawed, keys, j, key, frozVal;
    for (i=0; i=0) ? thawedObjects[frozVal] : atomsTable[-frozVal];
        }
    }
    return thawedObjects;
};
function unforestify(forestified){ return thawForest(forestified)[0]; }

上述简化的`forestify`和`unforestify`版本可以在`siberiaUntyped.js`文件中找到(不到100行代码),它们没有被使用,但是提供了更容易学习的方法。简化版本和真实版本之间的主要区别是原子类型和`forestify`的非递归版本(有点难以阅读,不可否认),以防止处理非常大的对象(例如具有100,000个节点的链表)时出现堆栈溢出错误。

Douglas Crockford

为什么"siberia"比Douglas Crockford的`decycle.js`(2018版本)更快,速度提升是无限的:首先,它们有相似之处。上述的`discover`函数与`.decycle`中的`derez`函数相似。就像`discover`一样,`derez`本质上是深度优先搜索,如果再次遇到以前遇到的对象,则添加了一个停止条件。在这两种情况下,ES6特性`WeakMap`用于生成已知对象的查找表。但是,在`discover`和`derez`中,`WeakMap`的定义域是对象图的节点(即迄今为止发现的对象),但是这些对象在`discover`和`derez`中被映射到不同的内容。在`discover`中,它是对象索引,在`derez`中,它是从根到对象的路径。该路径是JavaScript代码,稍后在反序列化时使用`eval`进行解析。

例如,我们已经看到梨对象被`discover`映射为数字8,因为它是第8个被发现的对象。让我们看一下上面的对象图,追踪从根到梨的路径,即0 -> 1 -> 2 -> 3 -> 4 -> 5 -> 6 -> 7 -> 8。我们遇到的对象是根 -> 所有人 -> Joe -> Joe的水果 -> Apple -> Apple的爱好者 -> Jane -> Jane的水果 -> Pear。我们在这些对象之间看到的键(边的标签)是"people",0,"likes",0,"likedBy",1,"likes",1。

![fromRootToPear_IMG](https://mathheadinclouds.github.io/img/root2pear.png)

现在,在Douglas Crockford的版本中,我们可以执行以下操作:

dougsFridge = JSON.decycle(myData)
dougsFridge.fruits[2].$ref

结果将是以下字符串:

$["people"][0]["likes"][0]["likedBy"][1]["likes"][1]

毫不奇怪,siberia和Douglas Crockford的算法在众多可能的根到梨的路径中找到了相同的路径。毕竟,两者都是深度优先搜索,并添加了“已见对象”的停止条件,再加上存储一些东西。

区别在于,存储路径占用的空间是与涉及的节点数成线性关系的,这是无限的,而从路径返回到解冻对象的时间是与涉及的节点数成线性关系的。另一方面,在siberia中,存储路径的信息仅占用常量空间(只存储一个整数),从该整数返回到解冻对象只是一个数组查找,这需要常量时间。

此外,使用`eval`和正则表达式可能会很慢(尤其是后者),进一步降低运行时性能(这个问题远不及前一个问题严重)。

这里有一个展示siberia和Douglas Crockford的cycle.js之间速度比较的速度测试页面

这样做得很好。有没有可能将其放入一个带有友好许可证的git存储库中?我还想知道它与内置的JSON.stringify()相比如何。

0
0 Comments

问题出现的原因是在将对象从服务器传递到客户端的JavaScript时,对象存在循环引用的情况。循环引用是指对象之间相互引用,形成一个闭环,导致无法正常序列化和反序列化对象。

为了解决这个问题,可以使用"json-cycle"包来处理循环引用。该包可以在https://www.npmjs.com/package/json-cycle上找到详细文档。

下面是一个针对该问题的使用示例:

安装"json-cycle"包:

npm i json-cycle

将对象序列化为JSON字符串并发送:

const strJsonTree = JSON.stringify(jc.decycle(a));
console.log(strJsonTree);
send(strJsonTree);

接收到字符串后,将其反序列化为对象:

const obj = jc.retrocycle(JSON.parse(somestring));

如果使用TypeScript,可能需要在一个d.ts文件中声明模块,因为在npm上找不到/json-cycle:

declare module "json-cycle" {
  export function decycle(object: any, replacer?: (key: string, value: any) => any, depth?: number): any;
  export function retrocycle($: any): any;
}

通过使用"json-cycle"包,可以在保留循环引用的同时,将对象从服务器传递到客户端的JavaScript中。

0
0 Comments

问题的出现的原因是在将对象从服务器传递到客户端的JavaScript时,如果对象存在循环引用,常规的JSON序列化和反序列化方法会导致循环引用丢失,无法保持对象的循环性。

解决方法是使用Douglas Crockford提供的解决方案,即使用Cycle.js库。在使用Cycle.js时,首先使用`decycle`方法来序列化对象,然后使用`retrocycle`方法来反序列化恢复对象的循环引用。

以下是使用Cycle.js的示例代码:

var jsonString = JSON.stringify(JSON.decycle(parent));
var restoredObject = JSON.retrocycle(JSON.parse(jsonString));

可以在JSFiddle上查看示例:[JSFiddle](http://jsfiddle.net/HY925/2/)

还有其他类似实现的库,比如WebReflection的Circular-JSON库,但是Cycle.js更小且在实际使用中效果更好。

如果遇到`retrocycle`后无法获取相同对象的问题,可以参考其他回答中提供的方法,可能更适用于大型对象。

如果对Crockford的解决方案有疑问或建议,可以通过他的官方论坛或在GitHub上提交问题,他通常会积极回应并根据情况进行改进。

对于安装Cycle.js后出现`JSON.decycle is not a function`的TypeError错误,可能需要通过引入相应的方法进行导入。具体如何导入可以参考库的文档或者其他相关资源。

0