探寻 JavaScript 精度问题以及解决方案

简介:

阅读完本文可以了解到 0.1+0.2 为什么等于 0.30000000000000004 以及 JavaScript 中最大安全数是如何来的。

十进制小数转为二进制小数方法

拿 173.8125 举例如何将之转化为二进制小数。

① 针对整数部分 173,采取 除2取余,逆序排列。

 
  1. 173 / 2 = 86 ... 1

  2. 86 / 2 = 43 ... 0

  3. 43 / 2 = 21 ... 1   ↑

  4. 21 / 2 = 10 ... 1   | 逆序排列

  5. 10 / 2 = 5 ... 0    |

  6. 5 / 2 = 2 ... 1     |

  7. 2 / 2 = 1 ... 0

  8. 1 / 2 = 0 ... 1

得整数部分的二进制为 10101101。

② 针对小数部分 0.8125,采用 乘2取整,顺序排列。

 
  1. 0.8125 * 2 = 1.625  |

  2. 0.625 * 2 = 1.25    | 顺序排列

  3. 0.25 * 2 = 0.5      |

  4. 0.5 * 2 = 1         ↓

得小数部分的二进制为 1101。

③ 将前面两部的结果相加,结果为 10101101.1101。

小心,二进制小数丢失了精度!

根据上面的知识,将十进制小数 0.1 转为二进制:

 
  1. 0.1 * 2 = 0.2

  2. 0.2 * 2 = 0.4 // 注意这里

  3. 0.4 * 2 = 0.8

  4. 0.8 * 2 = 1.6

  5. 0.6 * 2 = 1.2

  6. 0.2 * 2 = 0.4 // 注意这里,循环开始

  7. 0.4 * 2 = 0.8

  8. 0.8 * 2 = 1.6

  9. 0.6 * 2 = 1.2

  10. ...

可以发现有限十进制小数 0.1 却转化成了无限二进制小数 0.00011001100...,可以看到精度在转化过程中丢失了!

能被转化为有限二进制小数的十进制小数的最后一位必然以 5 结尾(因为只有 0.5 * 2 才能变为整数)。所以十进制中一位小数 0.1~0.9 当中除了 0.5 之外的值在转化成二进制的过程中都丢失了精度。

推导 0.1 + 0.2 为何等于 0.30000000000000004

在 JavaScript 中所有数值都以 IEEE-754 标准的 64bit 双精度浮点数进行存储的。先来了解下 IEEE-754 标准下的双精度浮点数。

2108e0406f9bc2498c538716bb9fe5a4b5784356

这幅图很关键,可以从图中看到 IEEE-754 标准下双精度浮点数由三部分组成,分别如下:

  • sign(符号): 占 1 bit,表示正负。

  • exponent(指数): 占 11 bit,表示范围。

  • mantissa(尾数): 占 52 bit,表示精度,多出的末尾如果是 1 需要进位。

推荐阅读 JavaScript 浮点数陷阱及解法,阅读完该文后可以了解到以下公式的由来。

7af88a7f937e82de7f8fce344a31b5acc673b13b

精度位总共是 53 bit,因为用科学计数法表示,所以首位固定的 1 就没有占用空间。即公式中 (M + 1) 里的 1。另外公式里的 1023 是 2^11 的一半。小于 1023 的用来表示小数,大于 1023 的用来表示整数。

指数可以控制到 2^1024 - 1,而精度最大只达到 2^53 - 1,两者相比可以得出 JavaScript 实际可以精确表示的数字其实很少。

0.1 转化为二进制为 0.0001100110011...,用科学计数法表示为 1.100110011...x2^(-4),根据上述公式, S 为 0(1 bit), E 为 -4+1023,对应的二进制为 01111111011(11 bit), M 为 1001100110011001100110011001100110011001100110011010(52 bit,另外注意末尾的进位), 0.1 的存储示意图如下:

cf7bf6b9b8078eec02db7231ebab93742183c6bc

同理, 0.2 转化为二进制为 0.001100110011...,用科学计数法表示为 1.100110011...x2^(-3),根据上述公式, E 为 -3+1023,对应的二进制为 01111111100, M 为 1001100110011001100110011001100110011001100110011010, 0.2 的存储示意图如下:

0b5f4f2b26d210d0042d593184b4f46668da9755

0.1+0.2 即 2^(-4) x 1.1001100110011001100110011001100110011001100110011010 与 2^(-3) x 1.1001100110011001100110011001100110011001100110011010 之和

 
  1. // 计算过程

  2. 0.00011001100110011001100110011001100110011001100110011010

  3. 0.0011001100110011001100110011001100110011001100110011010

  4. // 相加得

  5. 0.01001100110011001100110011001100110011001100110011001110

0.01001100110011001100110011001100110011001100110011001110 转化为十进制就是 0.30000000000000004。验证完成!

JavaScript 的最大安全数是如何来的

根据双精度浮点数的构成,精度位数是 53bit。安全数的意思是在 -2^53~2^53 内的整数(不包括边界)与唯一的双精度浮点数互相对应。举个例子比较好理解:

 
  1. Math.pow(2, 53) === Math.pow(2, 53) + 1 // true

Math.pow(2,53) 竟然与 Math.pow(2,53)+1 相等!这是因为 Math.pow(2, 53) + 1 已经超过了尾数的精度限制(53 bit),在这个例子中 Math.pow(2,53) 和 Math.pow(2,53)+1 对应了同一个双精度浮点数。所以 Math.pow(2,53) 就不是安全数了。

最大的安全数为 Math.pow(2,53)-1,即 9007199254740991。

业务中碰到的精度问题以及解决方案

了解 JavaScript 精度问题对我们业务有什么帮助呢?举个业务场景:比如有个订单号后端 Java 同学定义的是 long 类型,但是当这个订单号转换成 JavaScript 的 Number 类型时候精度会丢失了,那没有以上知识铺垫那就理解不了精度为什么会丢失。

解决方案大致有以下几种:

1. 针对大数的整数可以考虑使用 bigint 类型(目前在 stage 3 阶段)。
2. 使用 bigNumber,它的思想是转化成 string 进行处理,这种方式对性能有一定影响。
3. 可以考虑使用 long.js,它的思想是将 long 类型的值转化成两个 32 位的双精度类型的值。

4. 针对小数可以考虑 JavaScript 浮点数陷阱及解法 里面提到的方案。


原文发布时间为:2018-11-08 本文作者:牧云云 

本文来自云栖社区合作伙伴“前端大学”,了解相关信息可以关注“前端大学”。

相关文章
|
JavaScript 前端开发
JS浮点数精度问题及高精度小数运算:BigNumber解决方案
JS浮点数精度问题及高精度小数运算:BigNumber解决方案
2305 0
|
开发框架 前端开发 JavaScript
C# 6.0+JavaScript云LIS系统源码  云LIS实验室信息管理新型解决方案
云LIS是为区域医疗提供临床实验室信息服务的计算机应用程序,可协助区域内所有临床实验室相互协调并完成日常检验工作,对区域内的检验数据进行集中管理和共享,通过对质量控制的管理,最终实现区域内检验结果互认。其目标是以医疗服务机构为主体,以医疗资源和检验信息共享为目标,集成共性技术及医疗服务关键技术,建立区域协同检验,最大化利用有限的医疗卫生资源。
444 1
|
监控 数据可视化 安全
如何使用webgl(three.js)实现煤矿隧道、井下人员定位、掘进面、纵采面可视化解决方案——第十九课(一)
three.js、webgl、3D煤矿隧道、三维井下人员定位、掘进面三维可视化、纵采面可视化、采集面可视化展示、设备检测、数字孪生、物联网3D、3d建筑、3d库房,bim管理系统
1040 1
|
小程序 JavaScript 前端开发
uni-app开发微信小程序:四大解决方案,轻松应对主包与vendor.js过大打包难题
uni-app开发微信小程序:四大解决方案,轻松应对主包与vendor.js过大打包难题
5351 1
|
前端开发 JavaScript API
Howler.js:音频处理的轻量级解决方案
Howler.js:音频处理的轻量级解决方案
2452 0
|
前端开发 JavaScript 开发者
除了 Generator 函数,还有哪些 JavaScript 异步编程解决方案?
【10月更文挑战第30天】开发者可以根据具体的项目情况选择合适的方式来处理异步操作,以实现高效、可读和易于维护的代码。
|
前端开发 JavaScript 开发者
JS 异步解决方案的发展历程以及优缺点
本文介绍了JS异步解决方案的发展历程,从回调函数到Promise,再到Async/Await,每种方案的优缺点及应用场景,帮助开发者更好地理解和选择合适的异步处理方式。
|
JavaScript 前端开发
js防抖函数返回值问题解决方案
本文介绍了如何在JavaScript中创建一个带有返回值的防抖函数,通过结合Promise来实现。这种防抖函数可以在事件触发一定时间后再执行函数,并能处理异步操作的返回值。文章提供了防抖函数的实现代码和如何在实际项目中使用该防抖函数的示例。
341 1
|
JavaScript 调度
Three.js开发秘籍:FlyControls的拖拽视角问题解决方案
Three.js开发秘籍:FlyControls的拖拽视角问题解决方案
427 0
|
JavaScript 前端开发
vue常见报错解决方案 | javascript heap out of memory
vue常见报错解决方案 | javascript heap out of memory
2103 0