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

参考

相关推荐
Мартин.2 小时前
[Meachines] [Easy] Sea WonderCMS-XSS-RCE+System Monitor 命令注入
前端·xss
昨天;明天。今天。3 小时前
案例-表白墙简单实现
前端·javascript·css
数云界3 小时前
如何在 DAX 中计算多个周期的移动平均线
java·服务器·前端
风清扬_jd3 小时前
Chromium 如何定义一个chrome.settingsPrivate接口给前端调用c++
前端·c++·chrome
安冬的码畜日常3 小时前
【玩转 JS 函数式编程_006】2.2 小试牛刀:用函数式编程(FP)实现事件只触发一次
开发语言·前端·javascript·函数式编程·tdd·fp·jasmine
ChinaDragonDreamer3 小时前
Vite:为什么选 Vite
前端
小御姐@stella3 小时前
Vue 之组件插槽Slot用法(组件间通信一种方式)
前端·javascript·vue.js
GISer_Jing3 小时前
【React】增量传输与渲染
前端·javascript·面试
GISer_Jing3 小时前
WebGL在低配置电脑的应用
javascript
eHackyd3 小时前
前端知识汇总(持续更新)
前端