JS 中的浮点数计算经常会看到这样问题:为什么 0.1 + 0.2 不等于 0.3 ?
在浏览器控制台执行 0.1 + 0.2 会得到一个奇怪的结果:0.30000000000000004
为何结果会是这么奇怪的一个数字?人类瞄一眼就知道的结果,为啥交给 JS 会得出这么奇怪的结果?
都知道程序的世界就是二进制的天下,在电脑的 CPU 运算时,所有的十进制都需要转为二进制计算。
十进制转二进制
十进制整数转二进制一般使用除 2 取余法,比如 35 换算二进制:
js
35 / 2 = 17 余数 1
17 / 2 = 8 余数 1
8 / 2 = 4 余数 0
4 / 2 = 2 余数 0
2 / 2 = 1 余数 0
1 / 2 = 0 余数 1
最终结果将余数倒序排列得到 100011
而小数换算二进制则使用乘 2 取整法,比如 0.03125 :
js
0.03125 * 2 = 0.0625 取整 0
0.0625 * 2 = 0.125 取整 0
0.125 * 2 = 0.25 取整 0
0.25 * 2 = 0.5 取整 0
0.5 * 2 = 1 取整 1
将最终的计算结果正序排列得到 0.00001
除了整数和小数外,还涉及到负数转换,有兴趣可以了解下 IEEE 754 标准(JS 数值运算基于 IEEE 754 标准)。
0.1 与 0.2
各种编程语言的 0.1 + 0.2 结果:https://0.30000000000000004.com/#ada
至于为什么会是 0.30000000000000004 这个结果,百度一搜一大把的文章,本文就不再赘述。
一句话总结就是 0.1 和 0.2 在转为 二进制 后是一个无限循环的结果(类似十进制中的 1/3),而 IEEE 754 标准中储存位数是有限的,在处理这种无限循环的时,会进行舍入处理,就会造成计算精度丢失,在一系列 舍入 和 规格化数 后就得出了这么一个结果。
处理办法
同一个作者,写了三个这种运算模块,npm 的周下载量都在千万级别:
big.js github 地址:https://github.com/MikeMcl/big.js
npm 周下载量 2 千万左右:
bignumber.js github 地址:https://github.com/MikeMcl/bignumber.js
npm 周下载量 1.5 千万左右:
decimal.js github 地址:https://github.com/MikeMcl/decimal.js
npm 周下载量: 2 千万左右。
三者区别:
官方文档:https://github.com/MikeMcl/big.js/wiki
总结:
包体积 big.js < bignumber.js < decimal.js
big.js 适合基础的十进制运算,比如简单的金融计算等。
bignumber.js 支持二进制运算,适合一些加密计算场景。
decimal.js 支持二进制和三角函数运算,适合一些科学计算场景。
其他作者写的数学计算模块:
https://github.com/josdejong/mathjs
star 数量: 14762
npm 周下载量: 1 百万左右
瞅了一眼,好像内部也是依赖的 decimal.js
使用示例:
html
<script src="https://registry.npmmirror.com/big.js/7.0.1/files/big.js"></script>
<script src="https://registry.npmmirror.com/bignumber.js/9.3.0/files/bignumber.js"></script>
<script src="https://registry.npmmirror.com/decimal.js/10.5.0/files/decimal.js"></script>
<script>
(() => {
// 加法
console.log('原生 JS 计算:', 0.1 + 0.2) // 0.30000000000000004
// 减法
console.log('原生 JS 计算:', 0.3 - 0.1) // 0.19999999999999998
// 乘法
console.log('原生 JS 计算:', 0.2 * 0.1) // 0.020000000000000004
// 除法
console.log('原生 JS 计算:', 0.3 / 0.1) // 2.9999999999999996
})();
(() => {
const x = new Big(0.1)
const y = new Big(0.2)
const z = new Big(0.3)
// 加法
console.log('big.js 插件计算:', x.plus(y).toNumber()) // 0.3
console.log('big.js 插件计算:', x.plus(y).toString()) // '0.3'
// 减法
console.log('big.js 插件计算:', z.minus(x).toNumber()) // 0.2
// 乘法
console.log('big.js 插件计算:', x.times(y).toNumber()) // 0.02
// 除法
console.log('big.js 插件计算:', z.div(x).toNumber()) // 3
})();
(() => {
const x = new BigNumber(0.1)
const y = new BigNumber(0.2)
const z = new BigNumber(0.3)
// 加法
console.log('bignumber.js 插件计算:', x.plus(y).toNumber()) // 0.3
console.log('bignumber.js 插件计算:', x.plus(y).toString()) // '0.3'
// 减法
console.log('bignumber.js 插件计算:', z.minus(x).toNumber()) // 0.2
// 乘法
console.log('bignumber.js 插件计算:', x.times(y).toNumber()) // 0.02
// 除法
console.log('bignumber.js 插件计算:', z.div(x).toNumber()) // 3
})();
(() => {
const x = new Decimal(0.1)
const y = new Decimal(0.2)
const z = new Decimal(0.3)
// 加法
console.log('decimal.js 插件计算:', x.plus(y).toNumber()) // 0.3
console.log('decimal.js 插件计算:', x.plus(y).toString()) // '0.3'
// 减法
console.log('decimal.js 插件计算:', z.minus(x).toNumber()) // 0.2
// 乘法
console.log('decimal.js 插件计算:', x.times(y).toNumber()) // 0.02
// 除法
console.log('decimal.js 插件计算:', z.div(x).toNumber()) // 3
})();
</script>
也可以使用 npm 安装使用
js
// npm install big.js
// const Big = require('big.js');
import Big from 'big.js';
const x = new Big(0.1)
原生 JS 要处理浮点数计算也能做,只是比较麻烦,需要将计算的小数转为整数之后再进行计算,最后除以倍数。比如 0.1 + 0.2 可以转为 (1+2) / 10。
JS 支持的 最大整数 也是有边界的,在 −2的53次方 + 1 到 2的53次方 − 1 之间(即 -9007199254740991 到 9007199254740991)。
超出这个边界计算也会出问题,比如:
js
(() => {
console.log('超出边界计算', 2**53) // 9007199254740992
console.log('超出边界计算', 2**53 - 1) // 9007199254740991
console.log('超出边界计算', 2**53 - 1.1) // 9007199254740991
console.log('超出边界计算', 2**53 + 1) // 9007199254740992
console.log('超出边界计算', 2**53 + 1.1) // 9007199254740994
})();
超大数计算也可以使用 big.js 处理:
js
(() => {
const x = (new Big(2)).pow(53)
const y = new Big(1.1)
console.log('加法超出边界计算', x.plus(y).toString()) // 9007199254740993.1
console.log('减法超出边界计算', x.minus(y).toString()) // 9007199254740990.9
})();
写在最后
凡使用 IEEE 754 标准的编程语言,都存在浮点数计算问题,只是其他语言有内置的解决方案,而 JS 最初的设计思想是用于浏览器交互,所以没有内置解决方案,好在有热心作者开源的插件用于解决浮点数计算问题。
在使用 JS 计算时,需特别小心浮点数问题,能交给后端处理就交出去~~