探寻 JavaScript 精度问题

阅读完本文可以了解到 `0.1 + 0.2` 为什么等于 `0.30000000000000004` 以及 JavaScript 中最大安全数是如何来的。 ### 十进制小数转为二进制小数方法 拿 173.8125 举例如何将之转化为二进制小数。 ①. 针对整数部分 173,采取`除 2 取余,逆序排列`; ```js 173 / 2 = 86 ... 1 86 / 2 = 43 ... 0 43 / 2 = 21 ... 1 ↑ 21 / 2 = 10 ... 1 | 逆序排列 10 / 2 = 5 ... 0 | 5 / 2 = 2 ... 1 | 2 / 2 = 1 ... 0 1 / 2 = 0 ... 1 ``` 得整数部分的二进制为 `10101101`。 ②. 针对小数部分 0.8125,采用`乘 2 取整,顺序排列`; ```js 0.8125 * 2 = 1.625 | 0.625 * 2 = 1.25 | 顺序排列 0.25 * 2 = 0.5 | 0.5 * 2 = 1 ↓ ``` 得小数部分的二进制为 `1101`。 ③. 将前面两部的结果相加,结果为 `10101101.1101`; ### 小心,二进制小数丢失了精度! 根据上面的知识,将十进制小数 `0.1` 转为二进制: ```js 0.1 * 2 = 0.2 0.2 * 2 = 0.4 // 注意这里 0.4 * 2 = 0.8 0.8 * 2 = 1.6 0.6 * 2 = 1.2 0.2 * 2 = 0.4 // 注意这里,循环开始 0.4 * 2 = 0.8 0.8 * 2 = 1.6 0.6 * 2 = 1.2 ... ``` 可以发现有限十进制小数 `0.1` 却转化成了无限二进制小数 `0.00011001100...`,可以看到精度在转化过程中丢失了! 能被转化为有限二进制小数的十进制小数的最后一位必然以 5 结尾(因为只有 0.5 * 2 才能变为整数)。所以十进制中一位小数 `0.1 ~ 0.9` 当中除了 `0.5` 之外的值在转化成二进制的过程中都丢失了精度。 ### 推导 0.1 + 0.2 为何等于 0.30000000000000004 在 JavaScript 中所有数值都以 IEEE-754 标准的 `64 bit` 双精度浮点数进行存储的。先来了解下 IEEE-754 标准下的[双精度浮点数](https://zh.wikipedia.org/wiki/%E9%9B%99%E7%B2%BE%E5%BA%A6%E6%B5%AE%E9%BB%9E%E6%95%B8)。 ![](http://oqhtscus0.bkt.clouddn.com/dda7d5b38676abfa13afb344f8a792ed.jpg-300) 这幅图很关键,可以从图中看到 IEEE-754 标准下双精度浮点数由三部分组成,分别如下: * sign(符号): 占 1 bit, 表示正负; * exponent(指数): 占 11 bit,表示范围; * mantissa(尾数): 占 52 bit,表示精度,多出的末尾如果是 1 需要进位; 推荐阅读 [JavaScript 浮点数陷阱及解法](https://github.com/camsong/blog/issues/9),阅读完该文后可以了解到以下公式的由来。 ![](http://oqhtscus0.bkt.clouddn.com/5c268e4bd6e0bf2466598d9d5cb58a16.jpg-200) > 精度位总共是 53 bit,因为用科学计数法表示,所以首位固定的 1 就没有占用空间。即公式中 (M + 1) 里的 1。另外公式里的 1023 是 2^11 的一半。小于 1023 的用来表示小数,大于 1023 的用来表示整数。 > 指数可以控制到 2^1024 - 1,而精度最大只达到 2^53 - 1,两者相比可以得出 JavaScript 实际可以精确表示的数字其实很少。 `0.1` 转化为二进制为 `0.0001100110011...`,用科学计数法表示为 `1.100110011... x 2^(-4)`,根据上述公式,`S` 为 `0`(1 bit),`E` 为 `-4 + 1023`,对应的二进制为 `01111111011`(11 bit),`M` 为 `1001100110011001100110011001100110011001100110011010`(52 bit,另外注意末尾的进位),`0.1` 的存储示意图如下: ![](http://oqhtscus0.bkt.clouddn.com/5b7c0dcc0b1770b6eed238e288eb4c0e.jpg-300) 同理,`0.2` 转化为二进制为 `0.001100110011...`,用科学计数法表示为 `1.100110011... x 2^(-3)`,根据上述公式,`E` 为 `-3 + 1023`,对应的二进制为 `01111111100`, `M` 为 `1001100110011001100110011001100110011001100110011010`, `0.2` 的存储示意图如下: ![](http://oqhtscus0.bkt.clouddn.com/cb0ef89aa0ed6e8d32b90d1a29cfc9e1.jpg-300) `0.1 + 0.2` 即 2^(-4) x 1.1001100110011001100110011001100110011001100110011010 与 2^(-3) x 1.1001100110011001100110011001100110011001100110011010 之和 ```js // 计算过程 0.00011001100110011001100110011001100110011001100110011010 0.0011001100110011001100110011001100110011001100110011010 // 相加得 0.01001100110011001100110011001100110011001100110011001110 ``` `0.01001100110011001100110011001100110011001100110011001110` 转化为十进制就是 `0.30000000000000004`。验证完成! ### JavaScript 的最大安全数是如何来的 根据双精度浮点数的构成,精度位数是 `53 bit`。安全数的意思是在 `-2^53 ~ 2^53` 内的整数(不包括边界)与唯一的双精度浮点数互相对应。举个例子比较好理解: ```js 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`。 ### 相关链接 * [代码之谜系列](http://justjavac.com/codepuzzle/2012/11/11/codepuzzle-float-who-stole-your-accuracy.html) * [IEEE-754 进制转换图生成](http://www.binaryconvert.com/convert_double.html) * [JavaScript 浮点数陷阱及解法](https://github.com/camsong/blog/issues/9): 推荐阅读 * [javascript 里最大的安全的整数为什么是2的53次方减一](https://www.zhihu.com/question/29010688)
posted @ 2018-10-03 15:56 牧云云 阅读(...) 评论(...) 编辑 收藏