好一个 JS Number,和它拼了

今天不聊典中典 0.1 + 0.2 !== 0.3,聊一些业务中更常见的痛点 😭

第一回合:处理时精度丢失

让我们从一个关于精度丢失的 bug 说起:

  1. 假设我们从接口中获取了一个 JSON 结构,就是像图上的 jsonCode 那样 ⬆️
  2. 需求中要求我们对其中的某个值进行修改,比如把 p2 修改为 true
  3. 随后我们把处理完的数据放在 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,从而导致了精度丢失!

处理方法

显式传入使用JSONbigtransformRequest 方法,避免大数的精度丢失:

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 类型执行任何复杂数字计算操作!!! 有的是踩不完的坑!

参考

相关推荐
知识分享小能手23 分钟前
Html5学习教程,从入门到精通,HTML5 简介语法知识点及案例代码(1)
开发语言·前端·javascript·学习·前端框架·html·html5
IT、木易26 分钟前
大白话React第二章深入理解阶段
前端·javascript·react.js
晚安72031 分钟前
Ajax相关
前端·javascript·ajax
图书馆钉子户33 分钟前
怎么使用ajax实现局部刷新
前端·ajax·okhttp
bin91531 小时前
DeepSeek 助力 Vue 开发:打造丝滑的单选按钮(Radio Button)
前端·javascript·vue.js·ecmascript·deepseek
qianmoQ1 小时前
第五章:工程化实践 - 第五节 - Tailwind CSS 常见问题解决方案
前端·css
那就可爱多一点点1 小时前
超高清大图渲染性能优化实战:从页面卡死到流畅加载
前端·javascript·性能优化
不能只会打代码2 小时前
六十天前端强化训练之第一天HTML5语义化标签深度解析与博客搭建实战
前端·html·html5
OpenTiny社区2 小时前
Node.js技术原理分析系列——Node.js的perf_hooks模块作用和用法
前端·node.js
菲力蒲LY3 小时前
输入搜索、分组展示选项、下拉选取,全局跳转页,el-select 实现 —— 后端数据处理代码,抛砖引玉展思路
java·前端·mybatis