在将带有循环引用的对象从服务器传递到客户端JavaScript时保持循环引用。
在将带有循环引用的对象从服务器传递到客户端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
,带有
循环引用?
问题的出现原因:在将带有循环引用的对象从服务器传递到客户端的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()相比如何。
问题出现的原因是在将对象从服务器传递到客户端的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中。
问题的出现的原因是在将对象从服务器传递到客户端的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错误,可能需要通过引入相应的方法进行导入。具体如何导入可以参考库的文档或者其他相关资源。