背景
大多时候,前端开发人员在进行业务开发的时候,会碰到数据计算的场景。但是,在 JavaScript 中存在计算精度的问题,比如最能体现这点的一个经典问题就是,0.1 + 0.2 !== 0.3。因此,我们在处理数据计算的时候,有时会碰到一些让人特别疑惑的问题,比如后端返回了一个超长的数字,尤其是使用 Long 类型定义的。在使用计算时,突然发现计算的结果不对,不是最后几位为 0,就是计算得不到预想的结果。其实,在这个问题上,前端和后端都是没有问题的,代码也是没问题的。这里涉及到了一个知识盲区,Number 的安全整数。
知识点
安全整数(safe integer)
在 JavaScript 中,能够准确表示的整数范围在 -253 到 253 之间(不含两个端点),超过这个范围,无法精确表示这个值,这使得 JavaScript 不适合进行科学和金融方面的精确计算。 一个安全整数是一个符合下面条件的整数:
- 可以准确地表示为一个IEEE-754双精度数字,
- 其IEEE-754表示不能是舍入任何其他整数以适应IEEE-754表示的结果。.
比如,253 - 1 是一个安全整数,它能被精确表示,在任何 IEEE-754 舍入模式(rounding mode)下,没有其他整数舍入结果为该整数。作为对比,253 就不是一个安全整数,它能够使用 IEEE-754 表示,但是 253 + 1 不能使用 IEEE-754 直接表示,在就近舍入(round-to-nearest)和向零舍入中,会被舍入为 253。 安全整数范围为 -(253 - 1)到 253 - 1 之间的整数,包含 -(253 - 1)和 253 - 1。我们也可以通过 Number.isSafeInteger() 方法用来判断传入的参数值是否是一个「安全整数」(safe integer)。 举个例子,
Number.isSafeInteger(3); // true
Number.isSafeInteger(Math.pow(2, 53)) // false
Number.isSafeInteger(Math.pow(2, 53) - 1) // true
Number.isSafeInteger(NaN); // false
Number.isSafeInteger(Infinity); // false
Number.isSafeInteger("3"); // false
Number.isSafeInteger(3.1); // false
Number.isSafeInteger(3.0); // true
Math.pow(2, 53); // 9007199254740992
Math.pow(2, 53) - 1; // 9007199254740991
Math.pow(2, 53) === Math.pow(2, 53) + 1; // true
Number.MIN_SAFE_INTEGER // -9007199254740991
-(Math.pow(2, 53) - 1) // -9007199254740991
Number.MAX_SAFE_INTEGER // 9007199254740991
Math.pow(2, 53) - 1 // 9007199254740991
上面的代码中,超过了 2 的 53 次方之后,一个数就不准确了。ES6 中引入了 Number.MAX_SAFE_INTEGER和 Number.MIN_SAFE_INTEGER 这两个常量,用来表示这个范围的上下限。从上面的代码中,也可以看出来,JavaScript 能够精确表示的极限。
解决方案
计算精度问题
了解了 JavaScript 中的计算精度缺陷之后,我们可以通过 mathjs 库来进行解决。例如
// 1. 引入 mathjs
npm install mathjs
// 2. 使用
import {
atan2, chain, derivative, e, evaluate, log, pi, pow, round, sqrt
} from 'mathjs'
// functions and constants
round(e, 3) // 2.718
atan2(3, -3) / pi // 0.75
log(10000, 10) // 4
sqrt(-4) // 2i
pow([[-1, 2], [3, 1]], 2) // [[7, 0], [0, 7]]
derivative('x^2 + x', 'x') // 2 * x + 1
// expressions
evaluate('12 / (2.3 + 0.7)') // 4
evaluate('12.7 cm to inch') // 5 inch
evaluate('sin(45 deg) ^ 2') // 0.5
evaluate('9 / 3 + 2i') // 3 + 2i
evaluate('det([-1, 2; 3, 1])') // -7
// chaining
chain(3)
.add(4)
.multiply(2)
.done() // 14
或者,可以通过我们自己封装的函数库 cloud-utils 里的 accAdd、accSub、accMul、accDiv 来进行数据计算。
JSON.parse() 遇到的大数据问题
后端返回的数据一般都是 JSON 格式的字符串,比如
const response ='{"num": 90071992547409999, "username": "jack"}';
JSON.parse(response); // { num: 90071992547410000, username: 'jack' }
我们一般在使用网络请求库,如 axios 的时候,会在内部使用 JSON.parse 把后端返回的数据转为 JavaScript 对象,便于前端开发人员使用。拿上面的例子来说,由于返回了超出安全整数的范围的数据,所以在使用 JSON.parse 进行解析的时候,就出现了问题。因此,我们需要借助第三方包来解决此问题,这里推荐使用 json-bigint。它会把超出 JavaScript 安全整数范围的数字转为一个 BigNumber 类型的对象,对象数据是它内部的一个算法处理之后的,我们要做的就是在使用的时候转为字符串来使用。
// 1. 安装 json-bigint
npm i json-bigint
// 2. 使用示例
var JSONbig = require('json-bigint');
var json = '{ "value" : 9223372036854775807, "v2": 123 }';
console.log('Input:', json);
// { "value" : 9223372036854775807, "v2": 123 }
console.log('');
console.log('node.js built-in JSON:');
var r = JSON.parse(json);
console.log('JSON.parse(input).value : ', r.value.toString());
// JSON.parse(input).value : 9223372036854776000
console.log('JSON.stringify(JSON.parse(input)):', JSON.stringify(r));
// JSON.stringify(JSON.parse(input)): {"value":9223372036854776000,"v2":123}
console.log('\n\nbig number JSON:');
var r1 = JSONbig.parse(json);
console.log('JSONbig.parse(input).value : ', r1.value.toString());
// JSONbig.parse(input).value : 9223372036854775807
console.log('JSONbig.stringify(JSONbig.parse(input)):', JSONbig.stringify(r1));
// JSONbig.stringify(JSONbig.parse(input)): {"value":9223372036854775807,"v2":123}
因此,我们需要在使用 axios 时做如下相关的改造
import axios from 'axios';
import JSONbig from 'json-bigint';
const request = axios.create({
transformResponse: [function (data) {
try {
// 转换成功,则返回转换后的结果
return JSONbig.parse(data);
} catch(err) {
return data;
}
}]
})