core-js 中的 JSON.parse source text access:以 `JSON.rawJSON` 与 reviver 上下文实现大整数无损解析
2026/9/12 5:34:13 网站建设 项目流程

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 sourceJSON.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.parsecontext.source参数、JSON.rawJSONJSON.isRawJSON以及配套的JSON.stringify补丁。读完本文,你将掌握如何在大整数、高精度十进制等场景下借助 source text 与 raw JSON 实现无损往返,并理解 core-js 内部自研 JSON 解析器与"原始片段占位"机制的工作原理。

提案背景:为什么需要"源码文本访问"

原生JSON.parse('9007199254740993')会先把字面量转成 Number 再调用 reviver,此时精度已经丢失——90071992547409939007199254740992在 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.parsecontext.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对象记录valueendsource与子节点),对象/数组走object()/array()分支,数字走number()分支并通过slice(source, start, end)截取原始片段,字符串走string()分支,true/false/nullkeyword()分支(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.rawJSONJSON.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; }

要点有三:

  1. 文本不能为空,且首尾不允许出现空白字符(、\t\n\r),否则抛SyntaxError('Unacceptable as raw JSON')
  2. 文本必须能被解析为 JSON 原始值(数字、字符串、布尔、null);对象与数组会被拒绝;
  3. 返回的对象以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.jses.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。其核心机制是占位标记替换

  1. 生成一个唯一占位符RAW_MARK = uid()(es.json.stringify.js);
  2. 在 replacer 回调中,凡是isRawJSONValue(value)的值(原生支持时直接用原值,否则用isRawJSON内部检测),就把value.rawJSON压入rawStrings数组,并以RAW_MARK + 序号的字符串占位返回(es.json.stringify.js);
  3. 序列化完成后,对输出文本做一遍扫描:遇到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,所以无论数字多大都能精确还原;
  • 序列化侧bigIntToRawJSONbigint值转换为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-jsonesnext.json.parseesnext.json.raw-json三个模块(见 packages/core-js/proposals/json-parse-with-source.js);
  • core-jscore-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)。parseraw-jsonis-raw-jsonstringify四个模块共同构成 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_JSONNO_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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询