大家好,很高兴又见面了,我是"高级前端进阶",由我带着大家一起关注前端前沿、深入前端底层技术,大家一起进步,也欢迎大家关注、点赞、收藏、转发!
记得几年前在面试字节的时候面试官就问过关于 bigInt 的问题,今天决定一探究竟。本文部分内容来自文章 《Why does JSON.parse corrupt large numbers and how to solve this?》,文章链接可以参考文末资料。
1.JSON.parse 丢失数字精度
大多数 Web 应用程序会处理来自服务器的数据,数据以 JSON 接收并解析为 JavaScript 对象或数组,以便前端可以读取属性并执行操作。 通常,数据是使用 JavaScript 内置的 JSON.parse 函数来解析的,非常快速和方便。
JSON 数据格式非常简单,也是 JavaScript 的子集。 所以与 JavaScript 完全可以互换。 很多时候,前端不会去怀疑 JavaScript 中的 JSON 会出现问题,但是有些情况却非常极端、非常危险。 下面是一个有效的 JSON 字符串:
{"count": 9123372036854000123}
当将其解析为 JavaScript 并读取 count 时,会得到如下的值:
9123372036854000000
很显然,此时解析的值是不准确的,最后三位数字被重置为零。
2. 为什么 JSON.parse 会损坏数字
类似 9123372036854000123 的长数字既是有效的 JSON,也是有效的 JavaScript。 然而,当 JavaScript 将值解析为数字时,就会出现问题。
这是因为,最初 JavaScript 只有一种数字类型 Number,是 64 位浮点值,类似于 C++、Java 或 C# 中的 Double 值,该浮点值可以存储大约 16 位数字。 因此,不能完全表示像 9123372036854000123 的 19 位数字。 在这种情况下,最后三位数字将丢失,从而损坏该值。
当以浮点值存储分数时,也会发生同样的情况。比如,当开发者在 JavaScript 中计算 1/3 时,结果如下:
0.3333333333333333
实际上,该值应该有无限位数的小数,但 JavaScript 数字在大约 16 位数字后结束。
那么 JSON 文档中像 9123372036854000123 的大数值是从哪里来的呢? 这是因为,其他语言,例如: Java 或 C# 等确实有其他数字数据类型(例如 Long)。 Long 是一个 64 位值,可以容纳最多约 20 位的整数值,而原因是其不需要像浮点值那样存储指数值(Exponential Value )。
因此,在类似 Java 的语言中,开发者可能会拥有一个无法在 JavaScript Number 类型中正确表示的 Long 值,或者在其他语言中等效的 Double 类型中也无法正确表示。
JavaScript 的 Number 还有一些更多的限制:该值可能会溢出或下溢。 例如,1e+500 变为无穷大,1e-500 变为 0。不过,这些限制在实际应用中很少成为问题。
3. 如何防止数字被 JSON.parse 损坏
第一个解决方案是 JSON.parse 有一个可选的 reviver 参数,允许开发者以不同的方式解析内容。 但问题是,首先将文本解析为数字,然后将其传递给 reviver 为时已晚。
JSON.parse(
'{"1": 1,"2": 2,"3": {"4": 4,"5": {"6": 6}}}',
function (k, v) {
console.log(k);
// 输出当前的属性名,从而得知遍历顺序是从内向外的,
// 最后一个属性名会是个空字符串。
return v;
// 返回原始属性值,相当于没有传递 reviver 参数。
});
因此,开发者无法使用内置的 JSON.parse,而必须使用不同的 JSON 解析器。 幸运的是,有各种优秀的解决方案可以开箱即用。下面是不同的优秀开源库,用于解决 JSON.parse 的诸多问题,大家可以依据实际情况自行选择。
3.1 json-bigint
JSON.parse 和 JSON.tringify 的实现,支持 bigints 。 基于 Douglas Crockford JSON.js 包和 bignumber.js 库。
var JSONbig = require('json-bigint');
var json = '{"value": 9223372036854775807,"v2": 123}';
console.log('Input:', json);
console.log('');
console.log('node.js built-in JSON:');
var r = JSON.parse(json);
console.log('JSON.parse(input).value :', r.value.toString());
console.log('JSON.stringify(JSON.parse(input)):', JSON.stringify(r));
console.log('\n\nbig number JSON:');
var r1 = JSONbig.parse(json);
console.log('JSONbig.parse(input).value :', r1.value.toString());
console.log('JSONbig.stringify(JSONbig.parse(input)):', JSONbig.stringify(r1));
输出结果如下:
Input: {"value" : 9223372036854775807, "v2": 123}
node.js built-in JSON:
JSON.parse(input).value : 9223372036854776000
JSON.stringify(JSON.parse(input)): {"value":9223372036854776000,"v2":123}
big number JSON:
JSONbig.parse(input).value : 9223372036854775807
JSONbig.stringify(JSONbig.parse(input)): {"value":9223372036854775807,"v2":123}
3.2 lossless-json
lossless-json 用于解析 JSON,而且不会有丢失数字信息的风险。基础用法如下:
import {parse, stringify} from 'lossless-json'
const text = '{"decimal":2.370,"long":9123372036854000123,"big":2.3e+500}'
// JSON.parse will lose some digits and a whole number:
console.log(JSON.stringify(JSON.parse(text)))
// '{"decimal":2.37,"long":9123372036854000000,"big":null}'
// WHOOPS!!!
// LosslessJSON.parse will preserve all numbers and even the formatting:
console.log(stringify(parse(text)))
// '{"decimal":2.370,"long":9123372036854000123,"big":2.3e+500}'
该库的工作方式与本机 JSON.parse 和 JSON.stringify 完全相同。 不同之处在于 lossless-json 保留了大数字的信息。 lossless-json 不是将数值解析为常规数字,而是解析为 LosslessNumber,这是一个将数值存储为字符串的轻量级类。 开发者可以使用 LosslessNumber 执行常规操作,当这会导致信息丢失时则抛出错误。
3.3 js-json-bigint
js-json-bigint 是一个 JavaScript 库,允许使用 BigInt 支持对 JSON 进行编码。 如果需要在服务器中使用 64 位整数,这个库可以满足需求,64 位整数将被解析为 bigint 。同时,该库没有任何依赖性,体积只有 443 字节。
js-json-bigint 的实现也非常简单,核心代码实现只有简单的 20 多行左右:
export function parseJSON(text, reviver) {
if (typeof text !== 'string') {
return null
}
return JSON.parse(text.replace(/([^\"]+\"\:\s*)(\d{16,})(\,\s*\"[^\"]+|}$)/g, '$1"$2n"$3'), (k, v) => {
if (typeof v === 'string' && /^\d{16,}n$/.test(v)) {
v = BigInt(v.slice(0, -1))
}
return typeof reviver === 'function' ? reviver(k, v) : v
})
}
export function stringifyJSON(value, replacer, space) {
return JSON.stringify(value, (k, v) => {
if (typeof v === 'bigint') {
v = v.toString() + 'n'
}
return typeof replacer === 'function' ? replacer(k, v) : v
}, space).replace(/([^\"]+\"\:\s*)(?:\")(\d{16,})(?:n\")(\,\s*\"[^\"]+|}$)/g, '$1$2$3')
}
4.JSON.parse 方案更多注意事项
如上所述,大多数三方库会将长数字直接解析为 JavaScript 相对较新的 BigInt 数据类型。但是,这也可能会给不知道这些数据类型的第三方库带来问题,因为三方库也必须首先将包含 BigInt 值的 JSON 数据转换为库可以理解的内容。
同时,即使不涉及第三方库,使用 BigInt 值也可能会导致棘手的问题。 当混合使用大整数和常规数字时,JavaScript 可以默默地将一种数字类型强制转换为另一种数字类型,从而导致错误。
const a = 91111111111111e3 // a regular number
const b = 91111111111111000n // a bigint
console.log(a == b)
// 返回 false (应该是 true)
console.log(a> b)
// 返回 true (应该是 false)
比如,以上示例会看到两个常量 a 和 b 持有相同的数值。 但一个是数字,另一个是 BigInt,使用 == 和 > 等常规运算符可能会导致错误的结果。
总之,最好的办法是从一开始就尽量避免与大数字打交道,同时为了防止陷入与 BigInt 数据类型相关的难以调试的问题,使用 TypeScript 显式定义数据模型会很有帮助。
不过,值得一提的是关于“JSON.parse source text access”的 TC39 proposal 提案已经被提出。该提案扩展了 JSON.parse 行为以授予 reviver 函数对输入源文本的访问权限并扩展 JSON.stringify 行为以支持原始 JSON 文本基元的对象占位符的提案。
const digitsToBigInt = (key, val, {source}) =>
/^[0-9]+$/.test(source) ? BigInt(source) : val;
const bigIntToRawJSON = (key, val) =>
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
参考资料
https://jsoneditoronline.org/indepth/parse/why-does-json-parse-corrupt-large-numbers/
https://github.com/sidorares/json-bigint
https://github.com/josdejong/lossless-json
https://github.com/nicolasparada/js-json-bigint
https://github.com/epoberezkin/json-source-map
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse
https://community.make.com/t/parse-json-from-api/18279
https://www.showwcase.com/article/14824/how-to-parse-json-in-javascript
https://www.youtube.com/watch?v=g0kOJk4hTnY
https://twitter.com/mgechev/status/1366621806560444418
https://github.com/tc39/proposal-json-parse-with-source
本文暂时没有评论,来添加一个吧(●'◡'●)