core-js 中的 JSON.parse source text access:以JSON.rawJSON与 reviver 上下文实现大整数无损解析
【免费下载链接】core-jsStandard Library项目地址: https://gitcode.com/GitHub_Trending/co/core-js
导读
JSON.parse的 reviver 在解析过程中只能拿到解析后的 JS 值,原始文本中的细节(例如超出Number.MAX_SAFE_INTEGER的大整数、-0、尾数格式)会被原生解析器提前丢弃。core-js 实现了 TC39 提案JSON.parse with source(JSON.parsesource text access),在 packages/core-js/modules/es.json.parse.js、es.json.raw-json.js、es.json.is-raw-json.js 与 es.json.stringify.js 中提供JSON.parse的context.source参数、JSON.rawJSON、JSON.isRawJSON以及配套的JSON.stringify补丁。读完本文,你将掌握如何在大整数、高精度十进制等场景下借助 source text 与 raw JSON 实现无损往返,并理解 core-js 内部自研 JSON 解析器与"原始片段占位"机制的工作原理。
提案背景:为什么需要"源码文本访问"
原生JSON.parse('9007199254740993')会先把字面量转成 Number 再调用 reviver,此时精度已经丢失——9007199254740993与9007199254740992在 IEEE 754 double 中不可区分。同样,JSON.stringify也无法直接输出超出安全范围的整数字面量,而BigInt又不被 JSON 语法支持。
JSON.parse with source提案(对应规范草案tc39.es/proposal-json-parse-with-source)从两个方向解决该问题:
- 读方向:
JSON.parse的 reviver 增加第三个参数context,其中context.source携带该值在原始文本中的精确片段; - 写方向:
JSON.rawJSON(text)创建"原始 JSON 值",JSON.stringify遇到它时不重新序列化,而是原样嵌入文本。
这样大整数等精确数据就能在"文本 → JS 值 → 文本"的全链路中无损往返。
API 签名一览
core-js 文档(docs/web/docs/features/proposals/json-parse-source-text-access.md)给出的 TypeScript 签名为:
namespace JSON { isRawJSON(O: any): boolean; // patched for source support parse(text: string, reviver?: (this: any, key: string, value: any, context: { source?: string }) => any): any; rawJSON(text: any): RawJSON; // patched for `JSON.rawJSON` support stringify(value: any, replacer?: Array<string | number> | (this: any, key: string, value: any) => any, space?: string | number): string | void; }四个内置方法的职责分别是:
| 方法 | 行为 |
|---|---|
JSON.parse(text, reviver) | 在原生解析器不支持 source 时被整体替换为自研解析器;reviver 第三参context.source提供原始文本片段 |
JSON.rawJSON(text) | 校验text是合法的原始 JSON 值文本,返回一个携带内部标记的 RawJSON 对象 |
JSON.isRawJSON(O) | 判断一个值是否为JSON.rawJSON创建的 RawJSON 对象 |
JSON.stringify(value, replacer, space) | 被补丁以支持 RawJSON 值的原样输出(同时修复 Symbol 转换与畸形 Unicode 问题) |
读方向:JSON.parse的context.source
core-js 在 es.json.parse.js 中通过特性检测决定是否启用补丁:
var NO_SOURCE_SUPPORT = fails(function () { var unsafeInt = '9007199254740993'; var source; nativeParse(unsafeInt, function (key, value, context) { source = context.source; }); return source !== unsafeInt; });即:如果原生JSON.parse的 reviver 无法拿到context.source,core-js 就用自研解析器整体替换JSON.parse。调用时,如果未传 reviver 且底层JSON.parse行为正确(PROPER_BASE_PARSE),则直接走原生路径以保留性能:
parse: function parse(text, reviver) { return PROPER_BASE_PARSE && !isCallable(reviver) ? nativeParse(text) : $parse(text, reviver); }当传入 reviver 时,core-js 会用自己的解析器逐字符扫描文本并建立语法树节点(Node对象记录value、end、source与子节点),对象/数组走object()/array()分支,数字走number()分支并通过slice(source, start, end)截取原始片段,字符串走string()分支,true/false/null走keyword()分支(es.json.parse.js)。随后internalize在递归调用 reviver 时,只有"未被修改且是原始值节点"的值才会把context组装为{ source: node.source }:
var context = unmodified && typeof node.source == 'string' ? { source: node.source } : {};这一设计意味着context.source只对 JSON 原始值(数字、字符串、布尔、null)有意义;对象与数组节点返回空的context。测试 tests/unit-global/es.json.parse.js 完整验证了这一行为:
parse('1234', spy); // source === '1234' parse('"1234"', spy); // source === '"1234"' parse('null', spy); // source === 'null' parse('{}', spy); // source === undefined parse('[]', spy); // source === undefined parse('9007199254740993', spy);// source === '9007199254740993'写方向:JSON.rawJSON与JSON.isRawJSON
JSON.rawJSON的校验规则
es.json.raw-json.js 的实现展示了它严格的入参约束:
rawJSON: function rawJSON(text) { var jsonString = toString(text); if (jsonString === '' || isWhitespace(at(jsonString, 0)) || isWhitespace(at(jsonString, jsonString.length - 1))) { throw new $SyntaxError(ERROR_MESSAGE); // 'Unacceptable as raw JSON' } var parsed = parse(jsonString); if (typeof parsed == 'object' && parsed !== null) throw new $SyntaxError(ERROR_MESSAGE); var obj = create(null); setInternalState(obj, { type: 'RawJSON' }); createProperty(obj, 'rawJSON', jsonString); return FREEZING ? freeze(obj) : obj; }要点有三:
- 文本不能为空,且首尾不允许出现空白字符(、
\t、\n、\r),否则抛SyntaxError('Unacceptable as raw JSON'); - 文本必须能被解析为 JSON 原始值(数字、字符串、布尔、null);对象与数组会被拒绝;
- 返回的对象以
null为原型(Object.create(null)),通过内部状态槽{ type: 'RawJSON' }打标,并把原始文本存放在自身的rawJSON属性中;在支持冻结的环境中该对象还会被Object.freeze冻结。
对应单元测试(tests/unit-global/es.json.raw-json.js)验证了rawJSON(1).rawJSON === '1'、冻结行为以及三种非法输入的抛错:
assert.throws(() => rawJSON('"qwe'), SyntaxError, 'invalid 1'); assert.throws(() => rawJSON({}), SyntaxError, 'invalid 2'); assert.throws(() => rawJSON(''), SyntaxError, 'invalid 3');JSON.isRawJSON的内部状态判定
es.json.is-raw-json.js 只是把 internals/is-raw-json.js 导出为静态方法。其判定完全基于内部状态槽而非外观:
module.exports = function isRawJSON(O) { if (!isObject(O)) return false; var state = getInternalState(O); return !!state && state.type === 'RawJSON'; };因此伪造{ rawJSON: '...' }的普通对象不会被误判为 RawJSON。
特性检测与原生优先
internals/native-raw-json.js 用于探测宿主环境是否已原生支持:
module.exports = !fails(function () { var unsafeInt = '9007199254740993'; var raw = JSON.rawJSON(unsafeInt); return !JSON.isRawJSON(raw) || JSON.stringify(raw) !== unsafeInt; });若环境原生可用,es.json.raw-json.js与es.json.is-raw-json.js中的forced: !NATIVE_RAW_JSON使补丁跳过,避免重复定义。
JSON.stringify如何原样输出 RawJSON
es.json.stringify.js 在原生stringify存在的前提下被补丁,触发条件为WRONG_SYMBOLS_CONVERSION || ILL_FORMED_UNICODE || !NATIVE_RAW_JSON。其核心机制是占位标记替换:
- 生成一个唯一占位符
RAW_MARK = uid()(es.json.stringify.js); - 在 replacer 回调中,凡是
isRawJSONValue(value)的值(原生支持时直接用原值,否则用isRawJSON内部检测),就把value.rawJSON压入rawStrings数组,并以RAW_MARK + 序号的字符串占位返回(es.json.stringify.js); - 序列化完成后,对输出文本做一遍扫描:遇到
RAW_MARK前缀的字符串,就用rawStrings中对应的原始文本替换(es.json.stringify.js)。
由于占位符会先被当作普通字符串加引号序列化,最后再整体替换,所以rawJSON('9007199254740993')最终以无引号的数字字面量出现在输出中,而非被转义或精度丢失。
此外,同一文件还顺带修复了两类原生实现问题:Symbol 值序列化不一致(WRONG_SYMBOLS_CONVERSION,覆盖 Edge/WebKit/V8 的差异)与畸形 Unicode(ILL_FORMED_UNICODE,将孤立代理项输出为\udxxx转义)。后者的行为可在 tests/unit-global/es.json.stringify.js 的Well‑formed JSON.stringify用例中验证。
实战示例:大整数无损往返
下面这个例子完整来自 core-js 文档(json-parse-source-text-access.md),演示读取与写入两条路径的组合用法:
function digitsToBigInt(key, val, { source }) { return /^\d+$/.test(source) ? BigInt(source) : val; } function bigIntToRawJSON(key, val) { return typeof val === 'bigint' ? JSON.rawJSON(String(val)) : val; } const tooBigForNumber = BigInt(Number.MAX_SAFE_INTEGER) + 2n; JSON.parse(String(tooBigForNumber), digitsToBigInt) === tooBigForNumber; // => true const wayTooBig = BigInt(`1${ '0'.repeat(1000) }`); JSON.parse(String(wayTooBig), digitsToBigInt) === wayTooBig; // => true const embedded = JSON.stringify({ tooBigForNumber }, bigIntToRawJSON); embedded === '{"tooBigForNumber":9007199254740993}'; // => true理解这段代码的关键:
- 解析侧:
digitsToBigInt接收 reviver 第三参{ source },当source匹配/^\d+$/(纯数字文本)时,直接用BigInt(source)重建精确整数。因为source是原始文本而非已被舍入的 Number,所以无论数字多大都能精确还原; - 序列化侧:
bigIntToRawJSON把bigint值转换为JSON.rawJSON(String(val)),stringify再把它原样嵌入 JSON 文本,最终得到不带引号的大数字面量9007199254740993。
如何接入与按需引入
Entry points
根据文档中的 Entry points 说明(对应仓库 docs/web/docs/features/proposals/json-parse-source-text-access.md),可按需引入:
core-js/proposals/json-parse-with-source core-js(-pure)/es|stable|actual|full/json/is-raw-json core-js(-pure)/es|stable|actual|full/json/parse core-js(-pure)/es|stable|actual|full/json/raw-json core-js(-pure)/es|stable|actual|full/json/stringify其中:
core-js/proposals/json-parse-with-source是聚合入口,内部依次加载esnext.json.is-raw-json、esnext.json.parse、esnext.json.raw-json三个模块(见 packages/core-js/proposals/json-parse-with-source.js);core-js与core-js-pure两个包均提供es(标准)、stable(稳定特性)、actual(当前引擎实际缺失)、full(全部)四档粒度,便于按需打包;- 除了
parse/raw-json/is-raw-json,还需要json/stringify入口才能获得 RawJSON 的原样输出能力。
典型用法(以 ESM 语法示意):
import 'core-js/proposals/json-parse-with-source'; // 或按需引入: import 'core-js/actual/json/raw-json'; import 'core-js/actual/json/is-raw-json'; import 'core-js/actual/json/parse'; import 'core-js/actual/json/stringify';也可以直接使用全量包import 'core-js';(或core-js-pure的对应入口)。esnext前缀的模块(esnext.json.*)对应提案阶段能力,可单独 import。
与标准 JSON 模块的关系
core-js 没有为JSON提供整体 polyfill(JSON对象仅在 IE7 及更早环境中缺失),而是"按当前标准修补已有实现"(见 docs/web/docs/features/ecmascript/json.md)。parse、raw-json、is-raw-json、stringify四个模块共同构成 ECMAScript JSON 特性的一部分,其中JSON.stringify的补丁还会顺带覆盖 well-formed stringify 与 Symbol 转换等历史引擎缺陷。若仅需标准层面的JSON.parse严格性修正(例如负零与尾部空白处理),可只引入es.json.parse相关入口。
边界与注意事项
context.source只对原始值有效:对象与数组节点拿到的是空context({}),这是由internalize中的typeof node.source == 'string'判断决定的;JSON.rawJSON拒绝对象与数组:传入{}、[]会抛SyntaxError;空字符串、首尾空白文本同样被拒绝;JSON.stringify(BigInt)仍然抛TypeError:必须配合 replacer(如bigIntToRawJSON)先把 bigint 转成 RawJSON,stringify本身不直接支持 bigint;- RawJSON 对象不可伪装:
JSON.isRawJSON依赖内部状态槽{ type: 'RawJSON' },普通对象即使带有rawJSON属性也会返回false; - 原生优先:在已原生支持该提案的引擎上,core-js 通过
NATIVE_RAW_JSON与NO_SOURCE_SUPPORT特性检测跳过补丁,避免重复打补丁。
小结
core-js 对JSON.parse with source提案的实现形成了完整的闭环:自研的 es.json.parse.js 解析器负责在 reviver 中提供context.source,es.json.raw-json.js 与 internals/is-raw-json.js 负责 RawJSON 值的创建与判定,es.json.stringify.js 通过占位标记机制实现原样输出。配合 tests/unit-global/es.json.parse.js、tests/unit-global/es.json.raw-json.js 与 tests/unit-global/es.json.stringify.js 中的完整用例,这套方案让大整数、高精度文本等"精确 JSON"场景在旧引擎上也能可靠工作。
【免费下载链接】core-jsStandard Library项目地址: https://gitcode.com/GitHub_Trending/co/core-js
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考