重要聲明:此博借鑒了阿里巴巴 Fastjson 的思想
『科普』:
- 對於web前端,JSON序列化可以說是在 與服務端通訊(ajax+json) ,和使用 localStorage(讀 + 寫) 時。
- 對於服務端,我相信絕大多數人遇到問題是在於輸出JSON序列化數據。
循環引用對象序列化?這似乎是一個老生常談的問題,但是99.9%的人所謂的『解決』,都是在『逃避』這個問題,不信你搜搜『循環引用 JSON』試試?
后端相關文章一定告訴你要『禁用循環引用檢測』,『截斷對象』和『配置序列化規則( 各種Filter )』等降( tao )魔( bi )大法
前端相關文章最多就是告訴你可以設置一下序列化深度。
於是導致了數據丟失,花大量時間在序列化和反序列化邏輯上做判斷和處理,出現許多難以維護的bug,浪費社會資源。
說到底,JSON不就是數據交換嗎?它不支持表示循環引用對象,那就對它的語法進行擴展,讓它能夠表示不就好了?迎刃而解。
如何表示循環引用/重復引用對象?阿里的Fastjson已經告訴了我們答案:
- 創建一個對象表示引用對象,它僅有一個 key="$ref",value=對象引用路徑
- 對象引用路徑使用 "$" 表示根對象的引用,使用 "[數字]" 表示數組元素,使用 ".property" 表示對象字段
- 形如{ "$ref":"$.key1.array[3].key" }
Fastjson 是 Java 的庫,服務於后端,我在這里用 TypeScript 手寫一下它的實現以便前端能夠享受這個人類寶貴的精神財富。
首先寫個序列化和反序列化方法:( serializeCircular 和 parseCircular )
The important declaration: This mind comes form Fastjson Lib (Alibaba).
basic things:
For the web front-end, JSON is usually used on conmunication with server part, and localStorage.
For the back-end, I think where the most people have problem is serialization.
Basic things done.
Serialization Circular? It seems to be a very familiar issue? But what 99.9% of people call "handle" is "escape".Or you can try searching "Circular JSON".
The information about back-end would tell you to "disable circular checking", "cut object" and "create filters" to "h( e )a( s )n( c )d( a )l( p )e" it.
The information about front-end would tell you to change/set the deep/level in the settings of serialization.
And all above would cause losing data, and you will take huge time at the further logics of serialization, fixing bugs. It's a sheer waste.
But all in all, JSON is just for data-exchanging. It doesn't support circular , why not expand the role to make it supports?
How to express circular or repeatition? The answer has already been in the Fastjson lib, which built by Alibaba:
Create an object to express the circular, which has only one property named "$ref" and the value of it is a path-expression, which express the position of the circular from the root.
It uses "$" to express the reference of the root, "[index:number]" to express the item of an array, and ".key" to express the key of an common object.
Just like { "$ref":"$.key1.array[3].key" }
But Fastjson is a Java Lab for back-end, so I implement it by TypeScript for front-end.
At the first, make serialization and parse function: ( serializeCircular and parseCircular )
const _parseCircular = (root: any, parent: any, objkey: string | number) => { const obj = parent[objkey]; if (null === obj || typeof obj !== "object") { // } else if (Array.isArray(obj)) { for (let i = 0; i < obj.length; i++) { _parseCircular(root, obj, i); } } else if (!!obj["$ref"]) { let paths = (obj["$ref"] as string).split(/\.|\[|\]/).filter(s => !!s); paths.shift(); parent[objkey] = paths.reduce((a, b) => a[b], root); } else { Object.keys(obj).forEach(key => { _parseCircular(root, obj, key); }); } }; const _serializeCircular = (parent: any, base: string, objkey: string | number, obj_key_map: Map<string, any>, result: any) => { const obj = parent[objkey]; if (null === obj || typeof obj !== "object") { result[objkey] = obj; } else if (obj_key_map.has(obj)) { result[objkey] = { $ref: obj_key_map.get(obj) }; } else { const endFix = Array.isArray(parent) ? `[${objkey}]` : `.${objkey}`; let objrefstr = `${base}${endFix}`; obj_key_map.set(obj, objrefstr); if (Array.isArray(obj)) { result = result[objkey] = []; for (let i = 0; i < obj.length; i++) { _serializeCircular(obj, objrefstr, i, obj_key_map, result); } } else { result = result[objkey] = {}; Object.keys(obj).forEach(key => { _serializeCircular(obj, objrefstr, key, obj_key_map, result); }); } } }; const serializeCircular = (root: any) => { const map = new Map(); map.set(root, "$"); if (Array.isArray(root)) { let result = [] as any[]; for (let i = 0; i < root.length; i++) { _serializeCircular(root, "$", i, map, result); } return result; } else if (null !== root && typeof root === "object") { let result = {}; Object.keys(root).forEach(key => { _serializeCircular(root, "$", key, map, result); }); return result; } else { return root; } }; const parseCircular = (root: any): any => { if (Array.isArray(root)) { for (let i = 0; i < root.length; i++) { _parseCircular(root, root, i); } } else if (null !== root && typeof root === "object") { Object.keys(root).forEach(key => { _parseCircular(root, root, key); }); } return root; };
然后你可以僅僅只是用在某些特定的地方 ,如 RPC 和 localStorage,或者直接替換掉原本的 JSON.stringify 和 JSON.parse
Then you can just use it at some special places. such as RPC and localStorage, Or just replace the original JSON.stringify and JSON.parse.
let stringifyInited = false; if (!stringifyInited && (stringifyInited = true)) { const oriStringify = JSON.stringify; const oriParse = JSON.parse; const serialize = serializeCircular; const parse = parseCircular; JSON.stringify = function (this: any) { const args = Array.from(arguments) as any; args[0] = serialize(args[0]); return oriStringify.apply(this, args); } as any; JSON.parse = function (this: any) { const args = Array.from(arguments) as any; let res = oriParse.apply(this, args); return parse(res); } as any; }
測試:
Test:
// 測試 test (() => { let abc = {} as any; abc.a = {}; abc.b = {}; abc.a.b = abc.b; abc.b.a = abc.a; let abcSerialization = JSON.stringify(abc); console.log(abcSerialization); let abcParse = JSON.parse(abcSerialization); console.log(abcParse.a === abcParse.b.a); })(); // {"a":{"b":{"a":{"$ref":"$.a"}}},"b":{"$ref":"$.a.b"}} // true
最后,需要 Javascript 版本的朋友們,使用 tsc 命令將 TypeScript 編譯成 JavaScript 即可。
At the end, my friends, please use "tsc" command to compile TypeScript to JavaScript if need a JavaScript version.
# 安裝 typescript / install typescript npm install -g typescript # 編譯 ts 文件 / compile ts file tsc typescript.ts
感謝閱讀 Thanks for reading