今天不聊典中典 0.1 + 0.2 !== 0.3,聊一些业务中更常见的痛点 😭
第一回合:处理时精度丢失
让我们从一个关于精度丢失的 bug 说起:
- 假设我们从接口中获取了一个 JSON 结构,就是像图上的 jsonCode 那样 ⬆️
- 需求中要求我们对其中的某个值进行修改,比如把 p2 修改为 true
- 随后我们把处理完的数据放在 monaco 编辑器中展示
看起来很简单的三个步骤!但是意外总是在不经意间闪现~
观察上图右侧可以发现 p1 的值由 变成了 11111111111111110000, 一下子少了四个末尾的 1!
问题原因
我们触发了一个精度丢失!目前看这是个小问题,先介绍下原理:
不同于其他高级语言有多种数值类型如:int/longint/float/double等,一直以来 JS 不分整数和浮点型,所有数字都使用浮点型来储存,它采用 IEEE 754 标准定义的 64 位浮点格式表示数字,如下图
-
第0位表示符号位,s,正数的话是0,负数的话是1
-
第1位到第11位表示指数部分,e
-
第12位到第63位表示尾数部分,f
其中最大值:2^53是这么存的:符号位:0,指数:53,尾数:1.00000...000(一共52个0)
所以 js 中的 number 最大能表示的数是2的53次方:9007 1992 5474 0992,超过了这个数就会产生不精确的现象。但如果只是以字符串的形式存储或传输的话,是不会造成这个问题的,罪魁祸首竟是 JSON.parse
和 JSON.stringify
!
可以发现当我们放弃使用 JSON.parse
,只是将字符串填入 Monaco editor,并不会导致精度丢失。
处理办法
使用 json-bigint轻松处理!
javascript
var JSONbig = require('json-bigint');
var json = '{ "value" : 9223372036854775807, "v2": 123 }';
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}
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}
第二回合:请求时精度丢失
请求一个接口时当我们在 data 中携带一个大数(like:123213324235436456654)
在控制台中我们可以发现,作为 request data 的值也发生了精度丢失!
怎么会这样?我们明明没有显式的执行 JSON.parse
和 JSON.stringify
,为什么也会导致精度丢失呢?
问题原因
本次 http 请求使用的是 axios@0.25.0 版本进行的,让我们一起看看源码中的 transformRequest
部分:
scss
// defaults.js
function stringifySafely(rawValue, parser, encoder) {
if (utils.isString(rawValue)) {
try {
(parser || JSON.parse)(rawValue);
return utils.trim(rawValue);
} catch (e) {
if (e.name !== 'SyntaxError') {
throw e;
}
}
}
return (encoder || JSON.stringify)(rawValue);
}
transformRequest: [function transformRequest(data, headers) {
normalizeHeaderName(headers, 'Accept');
normalizeHeaderName(headers, 'Content-Type');
if (utils.isFormData(data) ||
utils.isArrayBuffer(data) ||
utils.isBuffer(data) ||
utils.isStream(data) ||
utils.isFile(data) ||
utils.isBlob(data)
) {
return data;
}
if (utils.isArrayBufferView(data)) {
return data.buffer;
}
if (utils.isURLSearchParams(data)) {
setContentTypeIfUnset(headers, 'application/x-www-form-urlencoded;charset=utf-8');
return data.toString();
}
if (utils.isObject(data) || (headers && headers['Content-Type'] === 'application/json')) {
setContentTypeIfUnset(headers, 'application/json');
return stringifySafely(data);
}
return data;
}],
当我们不显式的传入 transformRequest
方法时,axios 会执行默认的 transformRequest
逻辑 ,也就是说:
- 请求 data 类型为对象时
- 请求头为
application/json
时
axios 默认会执行一次 JSON.stringify
,从而导致了精度丢失!
处理方法
显式传入使用JSONbig
的transformRequest
方法,避免大数的精度丢失:
kotlin
axios({
url: '/api/v2/xxx',
method: 'post',
transformRequest: (data) => {
return JSONbig.stringify(data)
},
data: {
xx: 123213324235436456654
}
})
🌟 注意点:
即使我们使用了支持 bigint 的transformRequest
,但是我们在 Chrome DevTool 中还是会看到丢失精度的值,不过并不影响服务端实际拿到的数字,从日志中捞数据可以找到不丢失精度的值!
第三回合:渲染时精度丢失
接口获得的数据:
使用树形组件vue-json-editor
在页面渲染的数据:
可以发现同样出现了精度丢失!即使我们传入的是精度正常的数据!
问题原因
其实是因为一些组件自身就不支持对大数的渲染,使用方无法在数据层面改变这个问题
处理方法
使用 json-editor-vue
进行组件替换,在需要处理渲染大数的场景也要考虑组件的兼容性
限时返场:莫名其妙的 14.000000000000002
一个很简单的在图表中展示错误率的小需求,要求结果为四舍五入后两位数字的百分比:
看起来很简单,使用 toFixed
处理一下就好:
scss
const error_rate = Number(
(
error_count /
node_total_count
)?.toFixed(2)
) * 100
万万没想到被 QA 甩过来这样一个奇异的 case!
wtf?怎么可能能算出小数的?正常来说应该只会有 14 这个值吧?在浏览器中试试:
还真是 14.000000000000002,我明明执行了toFixed
呀?
原因其实是因为 0.14 的尾数发生了数据丢失,但是根据 JS 引擎定义的规则,最后丢失的值会进行 0 舍 1 入,下图是 0.14 的二进制表达:
最后的解决方案:
scss
const error_rate = (Number(
(
error_count /
node_total_count
)?.toFixed(2)
) * 100
)?.toFixed(0)
值的一提的是在 JS 中使用乘法获得一个意料之外的值并不是罕见的事,甚至变得更大和更小的 case 都有:
😅 忠告:请不要在 JS 中使用 Number 类型执行任何复杂数字计算操作!!! 有的是踩不完的坑!