2021-06-19
JSON & Object
keys 顺序
JSON 是无序(unordered)的。
对于 ES2015 及以后,按以下顺序:
- 按 integer key 升序
- 按 string key 插入顺序
- 按 symbol name 插入顺序
对于 ES2015 以前,无定义顺序规则。
API
众所周知,有 JSON.stringify
与 JSON.parse
。
不常用的一些技巧
JSON.stringify
若存在toJSON
,则会调用object.toJSON(key)
作为待 stringify 的结果;JSON.stringify(value[, replacer, space])
中的 replacer 可以是 数组 或(key, value: T) => T | undefined
- 为数组,相当于 pick 操作;
- 为函数,则取返回值为结果;
polyfill JSON.stringify
笔者实现的一个 JSON.stringify
polyfill 如下。
JavaScript
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
// lib-type.js const TYPE = { NUMBER: 0b0000000001, STRING: 0b0000000010, BOOLEAN: 0b0000000100, OBJECT: 0b0000001000, ARRAY: 0b0000010000, OBJECT_OR_ARRAY: 0b0000011000, SYMBOL: 0b0000100000, BIGINT: 0b0001000000, UNDEFINED: 0b0010000000, NULL: 0b0100000000, FUNCTION: 0b1000000000, }; const MAP = { "[object Number]": TYPE.NUMBER, "[object String]": TYPE.STRING, "[object Boolean]": TYPE.BOOLEAN, "[object Array]": TYPE.ARRAY, "[object Symbol]": TYPE.SYMBOL, "[object BigInt]": TYPE.BIGINT, "[object Undefined]": TYPE.UNDEFINED, "[object Null]": TYPE.NULL, "[object Function]": TYPE.FUNCTION, }; const getType = (x) => { const objectType = Object.prototype.toString.call(x); return MAP[objectType] || TYPE.OBJECT; }; module.exports = { TYPE, getType, };
JavaScript
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148
// lib-base.js const { TYPE, getType } = require("./lib-type"); const NULL_STR = "null"; const COMMA_STR = ","; const ESCAPE_RE = /"/g; const quote = (str) => { return '"' + str.replace(ESCAPE_RE, '\\"') + '"'; }; const _defaultReplacerFactory = (replacer) => { const set = new Set(replacer); return (key, value) => { return set.has(key) || key === "" ? value : undefined; }; }; function stringify(value, replacer, space = 0, adapter) { const push = adapter.onPush; const setUndefined = adapter.onUndefinedResult; // handle space when is number, up to 10 if (typeof space === "number") { space = Math.min(space, 10); } const getIndentString = (indent) => { const indentString = typeof space === "number" ? new Array(indent * space).fill(" ").join("") : new Array(indent).fill(space).join(""); return indentString; }; if (replacer) { replacer = Array.isArray(replacer) ? _defaultReplacerFactory(replacer) : replacer; } function _stringify(value, indent, type, contextType = 0, contextKey = "") { if (replacer) { value = replacer(contextKey, value); } let indentString = getIndentString(indent); type = type || getType(value); // handle Object String/Number/Boolean if (type & (TYPE.STRING | TYPE.NUMBER | TYPE.BOOLEAN)) { value = value.valueOf(); } // "toJSON" method if (type === TYPE.OBJECT && typeof value.toJSON === "function") { value = value.toJSON(contextKey); type = getType(value); } if (type & (TYPE.ARRAY | TYPE.OBJECT)) { if (contextType === 0 && type & TYPE.OBJECT_OR_ARRAY) { push(type === TYPE.OBJECT ? "{" : "["); indent++; indentString = getIndentString(indent); } let prefix = ""; for (const key in value) { // ignore symbol key, don't consider replacer function if (typeof key === "symbol") { continue; } // ignore non-num index in array if (type === TYPE.ARRAY && Number.isNaN(Number(key))) { continue; } let item = value[key]; const itemType = getType(item); // in object, ignore function / symbol / undefined if ( type === TYPE.OBJECT && itemType & (TYPE.FUNCTION | TYPE.SYMBOL | TYPE.UNDEFINED) ) { continue; } push(prefix); if (space) { push("\n" + indentString); } if (type === TYPE.OBJECT) { push(quote(key) + ": "); } if (itemType & TYPE.OBJECT_OR_ARRAY) { push(itemType === TYPE.OBJECT ? "{" : "["); } _stringify(item, indent + 1, itemType, type, key); if (itemType & TYPE.OBJECT_OR_ARRAY) { if (space) { push("\n" + indentString); } push(itemType === TYPE.OBJECT ? "}" : "]"); } prefix = COMMA_STR; } if (contextType === 0 && type & TYPE.OBJECT_OR_ARRAY) { if (space) { push("\n"); } push(type === TYPE.OBJECT ? "}" : "]"); } } else { // handle NaN / Infinity / -Infinity if ( (type === TYPE.NUMBER && value !== value) || value === Infinity || value === -Infinity ) { push(NULL_STR); } else if (type === TYPE.STRING) { push(quote(value)); } else if (type & (TYPE.FUNCTION | TYPE.SYMBOL | TYPE.UNDEFINED)) { // pure, transform result to undefined if (contextType === 0) { setUndefined(); } else { push(NULL_STR); } } else if (type === TYPE.NULL) { push(NULL_STR); } else if (type === TYPE.BIGINT) { // cannot support BigInt throw TypeError("BigInt value can't be serialized in JSON"); } else { push(value.toString()); } } } _stringify(value, 0); } module.exports = { stringify, };
JavaScript
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
const { stringify: baseStringify } = require("./lib-base"); const stringify = (value, replacer, space) => { let result = ""; const adapter = {}; adapter.onPush = async function (str) { result += str; }; adapter.onUndefinedResult = function () { result = undefined; }; baseStringify(value, replacer, space, adapter); return result; }; function main() { const result = stringify({ a: 1, b: { c: 3 } }, null, 4); console.log(result); } main();
JSON 与 stream
JSON.stringify
与 JSON.parse
实际上都可以流式处理,适合处理较大的 JSON 文件。
对于 stream 式的 JSON.stringify
,可以由以上的 polyfill 进行修改:
- 将 stringify 转为 generator 函数,对 push 操作进行 yield;
- 增加
pull(n)
API,能够控制 generator 流程; - 实现 Readable stream;
JavaScript
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77
function stringify(value, replacer, space = 0, adapter) { // push 改造 const push = function (str) { return str; }; const setUndefined = adapter.onUndefinedResult; // ... // 改造为 generator 以控制流程 function* _stringify(value, indent, type, contextType = 0, contextKey = "") { // ... if (type & (TYPE.ARRAY | TYPE.OBJECT)) { if (contextType === 0 && type & TYPE.OBJECT_OR_ARRAY) { // 涉及 push 的地方 yield yield push(type === TYPE.OBJECT ? "{" : "["); // ... } } else { // ... } } const iter = _stringify(value, 0); const exports = { pull() { const { value, done } = iter.next(); if (done) { return null; } return value; }, }; return exports; } class JSONStringify extends Readable { constructor(value, opts = {}) { super(opts); this.adapter = {}; this.adapter.onUndefinedResult = function () { throw Error("encode fail"); }; this.stringifyObject = stringify(value, null, 4, this.adapter); } _read(n) { // 此处没有使用 n,是因为实现中没有用 n 控制输出 // 而 stream 内部会做缓存,不需要手动做(即 push 不需要正好 push n 个字节) const str = this.stringifyObject.pull(n); this.push(str); } } async function main() { const jsonStream = new JSONStringify( { a: [12, 321, 42141, 512], b: { c: [{ dsaf: "2353252" }, { ddd: 23432 }], d: false, e: null, }, }, { // 控制 highWaterMark highWaterMark: 1, } ); const rs = createWriteStream("./result.json"); jsonStream.pipe(rs); } main();
对于 stream 式的 JSON.parse
,基于 jsonparse 实现。(实际上就是太懒不想造轮子= =)
- 实现 Duplex stream,将 JSON 字符串导入到 jsonparse 中,从
onValue
中获取 parse 后的对象;
JavaScript
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72
const Parser = require("jsonparse"); const { Transform, Readable, Writable, Duplex } = require("stream"); const jsonText = `{"a": [12,321,42141,512 ],"b": {"c": [{"dsaf": "2353252" },{"ddd": 23432 } ],"d": false,"e": null } }`; class FromRawData extends Readable { constructor(data) { super(); this.data = Buffer.from(data); } _read(n) { if (this.data.length === 0) { this.push(null); return; } const next = this.data.slice(0, n); this.data = this.data.slice(n); this.push(next); } } class JSONParse extends Duplex { constructor() { super({ readableObjectMode: true, }); this.queue = []; const parser = (this.parser = new Parser()); parser.onValue = (value) => { if (parser.stack.length === 0) { this.queue.push(value); this.queue.push(null); } }; } _read(n) { if (this.queue.length > 0) { this.queue.slice(0, n).forEach((item) => this.push(item)); this.queue.splice(0, n); } } _write(data, enc, next) { this.parser.write(data); next(); } } class Log extends Writable { constructor() { super({ objectMode: true, }); } _write(data, enc, cb) { console.log("[Log]", data); cb(); } } new FromRawData(jsonText).pipe(new JSONParse()).pipe(new Log());
更快的 JSON stringnify
fast-json-stringify 是一个快速的 JSON.stringify
实现,其主要基于 schema,即通过编写 schema,可以预先组成 stringnify 的代码,而不需要运行时 遍历 与 判断类型 等。
(当然,如果已经使用了这个库,可以试试 protocol buffers)