细节个人博客
细节个人博客
  • 首页
  • 前端开发
  • 后端开发
  • 其它
  • 关于我们

友情链接

  • 滑手佬的博客
  • 长歌的博客
  • _芋头丶Blog
  • 月落个人博客

灵魂拷问:0.1+0.2===0.3吗?

前端开发
细节
2022-05-09
1526
js
# 灵魂拷问:0.1 + 0.2 === 0.3 吗? 先说结果:在 JavaScript 中,0.1 + 0.2 是不等于 0.3 的。 ## 解决方案 1. 使用 `toFixed`: ```js parseFloat((0.1 + 0.2).toFixed(1)) === 0.3 // true ``` 2. 转为整数运算: ```js (0.1 * 10 + 0.2 * 10) / 10 === 0.3 // true ``` **注意:这两种方案只针对 0.1 + 0.2,其它情况不一定适用。** ## 为什么 0.1 + 0.2 !== 0.3? 在说为什么之前,我们需要了解一些前置知识。 ### 前置知识 在 JavaScript 中,浮点数是基于 [IEEE-754](https://baike.baidu.com/item/IEEE%20754/3869922?fr=aladdin) 中的**双精确度(64位)**标准进行存储的。也就是说,浮点数存储在内存空间中是一个64位的二进制小数的形式。 #### IEEE-754的64位双精度标准 ![image-20220505095438773.png](https://img-squad-prod.humandetail.com/inner/20220509SDAebSdy.png) + sign bit:符号位(1bit)**0表示正数,1表示负数** + exponent:指数位(11bits),正数,0(特殊值,11位全是0)、2047(特殊值,11位全是1) 或 [-1022,1023] + 1023 转换成二进制的结果,不够11位则在前面补0 + mantissa:有效尾数(52bits),尾数中有一个隐藏位“1”。原因是在二进制中,第一个有效数字必定是“1”,为了能够存储多一位精度,这个“1”并不会存储。 #### 科学计数法 当我们要标记或运算某个较大或较小且位数较多时,用科学计数法免去浪费很多的空间和时间。 比如:1000000000使用科学计数法表示为:1 * 10 ^ 9;0.001使用科学计数法表示为:1 * 10 ^-3;0.1使用科学计数法表示为:1 * 10^-1。 #### 指数位计算 0.1在科学计数法中,指数位为-4,所以 exponent 中存储是值是:`-4 + 1023 = 1019` ,转换成二进制为:`1111111011` ,不足11位,则在前面补0:`01111111011` #### 有效数计算过程 **0.1存储的值:** 在计算机中,有效数转换成二进制是通过乘以2(`*2`),如果结果大于1,则取乘积的小数部分继续乘以2,结果为0终止计算,最终结果是取每一次乘积中的整数位: ``` 0.1 转换成二进制过程 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.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 ... ``` 可以看出,后面是一段重复的运算过程,最后得出来的结果是: ``` 00011001100110011001100110011001100110011001100110011... ``` 也就是: ``` 0.00011001100110011001100110011001100110011001100110011... * 2^0 ``` 因为浮点数的存储首位必须是1,也就是`1.bbbbb.... * 2^n` 这种形式,所以我们需要将这个结果进行转换: ``` 0.1 => 0.00011111111100001100110011001100110011001100110011001100110011001... => 0.0001100110011001100110011001100110011001100110011001... * 2^0 => 1.100110011001100110011001100110011001100110011001... * 2^-4 ``` 而且有效数只能是52位,所以我们需要对结果进行截取,在十进制中我们有*四舍五入*的说法,而在二进制中是*零舍一入*: ``` 1.1001100110011001100110011001100110011001100110011001|1001 ``` 我们可以看到从小数点后面算第53位是1,需要向前进位: ``` 1.1001100110011001100110011001100110011001100110011010 ``` 并且小数点前面的 “1” 在存储时,会被隐藏,所以最终存储的52有效数是: ``` 1001100110011001100110011001100110011001100110011010 ``` **从上面的分析,可以得出 0.1 存储的值为:** ``` 0|01111111011|1001100110011001100110011001100110011001100110011010 // 去掉分隔线后 0011111110111001100110011001100110011001100110011001100110011010 ``` 因为存在一个截取过程,所以 **0.1在计算机中存储的值是和我们认知中的 0.1不相等的**。 **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.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 ... ``` 有效数结果为: ``` 00110011001100110011001100110011001100110011001100110011 ``` 也就是: ``` 0.00110011001100110011001100110011001100110011001100110011 * 2^0 => 1.1001... * 2^-3 ``` 可以得出 0.2 的存储值为: ``` 0|01111111100|1001100110011001100110011001100110011001100110011001|1001 // 截取且去掉分隔线后 0011111111001001100110011001100110011001100110011001100110011010 ``` #### 二进制四则运算规则 步骤: 1. [对阶](https://baike.baidu.com/item/%E5%AF%B9%E9%98%B6/10336161?fr=aladdin),使得两个数的小数位对齐; 2. 尾数求和,将两个数对阶之后按照定点的加减法运算规则计算; 3. 规格化,为了增加有效数的位数,必须将求和(差)之后的尾数进行规格化; 4. 舍入,64位存储中的有效数只能存储到52位,所以需要对53位之后的数值进入舍入(默认:0舍1入); 5. 溢出判断,判断计算结果是否存在溢出。 ### 0.1 + 0.2 的计算过程 #### 对阶 我们先看看科学计数法的运算规律: ``` 100 + 1000 = 1100 100 + 1000 = 1 * 10^2 + 1 * 10^3 = 0.1 * 10^3 + 1 * 10^3 = (0.1 + 1) * 10^3 = 1.1 * 10^3 ----- 12300 + 45600 = 57900 12300 + 45600 = 1.23 * 10^4 + 4.56 * 10^4 = (1.23 + 4.56) * 10^4 = 5.79 * 10^4 ``` 从上面的分析可以看出,科学计数法的计算时,可以先转换成最大阶码形式,提取公因式之后再进行运算。 所以我们在计算的时候,先要执行**对阶操作**,为了保证更好的精确度,我们采用的是小阶对大阶的形式来完成对阶 ``` 0.1 => 0|01111111011|1001100110011001100110011001100110011001100110011010 0.2 => 0|01111111100|1001100110011001100110011001100110011001100110011010 ``` **阶差:** ``` 01111111100 -01111111011 ------------ =00000000001 ``` 0.2 的阶码比 0.1 的阶码大,阶差为 `01111111100 - 01111111011 `, 结果是 1,所以 0.1 需要升一阶。 升阶的形式是尾数右移,每右移一位,阶码 + 1。 **注意:右移升阶时,会把隐藏位的 1 给挤出来** ``` 0.1 => (1).100110011001100110011001100110011001100110011001100110 * 2^01111111011 => (0).1100110011001100110011001100110011001100110011001100110 * 2^01111111100 ``` #### 尾数运算 ``` (0).11001100110011001100110011001100110011001100110011010 +(1).1001100110011001100110011001100110011001100110011010 ---------------------------------------------------------- =(10).01100110011001100110011001100110011001100110011001110 * 2^01111111100 ``` #### 规格化 有效数的第一位的取值只能是 1,需要对结果进行[规格化](https://baike.baidu.com/item/%E8%A7%84%E6%A0%BC%E5%8C%96), ``` (10).01100110011001100110011001100110011001100110011001110 * 2^01111111100 => (1).001100110011001100110011001100110011001100110011001110 * 2^01111111101 ``` #### 有效数截取 遵循*零舍一入*的原则: ``` (1).001100110011001100110011001100110011001100110011|001110 * 2^01111111101 => (1).001100110011001100110011001100110011001100110011|001110* 2^01111111101 ``` #### 溢出判断 指数位为 `01111111101`,并没溢出。 #### 0.1 + 0.2 的结果 所以 0.1 + 0.2 的结果为: ``` 0|01111111101|0011001100110011001100110011001100110011001100110011 // 移除分隔线 0011111111010011001100110011001100110011001100110011001100110011 ``` 转为十进制: ``` (1).0011001100110011001100110011001100110011001100110011 * 2^01111111101 // 降阶 =>(0).010011001100110011001100110011001100110011001100110011 * 2^0 // 截取 =>0.0100110011001100110011001100110011001100110011001101 * 2^0 // 转为 10 进制 => 0.30000000000000004 ``` ![image-20220505134148251.png](https://img-squad-prod.humandetail.com/inner/20220509ZWxENeIg.png) 浏览器控制台计算 0.1 + 0.2 的结果如下: ![image-20220505134225502.png](https://img-squad-prod.humandetail.com/inner/20220509Z2spBExD.png) 可以看出我们的计算是没有什么问题的,这也印证了为什么 0.1 + 0.2 不等于 0.3。 ## 为什么 0.1 + 0.3 === 0.4 0.1 + 0.2 我们都学会了怎么计算了,0.1 + 0.3 那不是信手拈来? 我们在上面已经计算出了 0.1 和 0.3 的小数位表示方式: ``` 0.1 => 0|01111111011|1001100110011001100110011001100110011001100110011010 0.3 => 0|01111111101|0011001100110011001100110011001100110011001100110011 ``` ### 对阶: 阶差为2,且 0.1是小阶: ``` 0.1 => 0|01111111011|1001100110011001100110011001100110011001100110011010 => 1.1001100110011001100110011001100110011001100110011010 * 2^01111111011 => 0.011001100110011001100110011001100110011001100110011010 * 2^01111111101 ``` ### 有效数计算 ``` 0.011001100110011001100110011001100110011001100110011010 * 2^01111111101 +1.0011001100110011001100110011001100110011001100110011 * 2^01111111101 --------------------------------------------------------------------------- =1.1001100110011001100110011001100110011001100110011001|10 * 2^01111111101 ``` ### 规格化 小数点右边是1,已经是规格数。 ### 有效数截取: ``` 1.1001100110011001100110011001100110011001100110011010 * 2^01111111101 ``` ### 溢出判断: 指数 `01111111101` 无溢出。 ### 01 + 0.3 的结果: ``` 0|01111111101|1001100110011001100110011001100110011001100110011010 // 移出分隔线 0011111111011001100110011001100110011001100110011001100110011010 ``` 转为十进制: ``` 1.1001100110011001100110011001100110011001100110011010 * 2^01111111101 => 0.011001100110011001100110011001100110011001100110011010 * 2^0 => 0.4 * 10^1 ``` 所以,0.1 + 0.3 是等于 0.4 的 ## 为什么说 toFixed 不安全? 我们先看看 toFixed 的一些结果: ![image-20220509165623344.png](https://img-squad-prod.humandetail.com/inner/20220509aLUdLebM.png) 通过上图,我们可以看出,`toFixed` 的截取有点诡异。 我们认知中的**四舍五入**的原则,运算结果应该是这样的: ```js 0.445.toFixed(2) // 0.45 0.446.toFixed(2) // 0.45 0.435.toFixed(2) // 0.44 0.4356.toFixed(2) // 0.44 ``` 我们再看看下面的例子: ![image-20220509171129487.png](https://img-squad-prod.humandetail.com/inner/20220509YzFpO2FB.png) 再来个对比: ![image-20220509171540025.png](https://img-squad-prod.humandetail.com/inner/20220509glGfA0YN.png) 为什么会造成这么诡异的情况呢?这是一种叫**四舍六入五成双**的进位方法:对于位数很多的近似数,当有效位确定后,其后面多余的数字应该舍去,只保留有效数最后一位,这种舍入规则是“四舍六入五成双”。 这种规则也叫“四舍六入五凑偶”: + 四舍:指小于或等于4时,直接舍去; + 六入:指大于或等于6时,舍去后进1; + 五凑偶:当5后面还有数字时,舍5进1;当5后面没有数字或为0时,需要分为以下几种情况: + 5前面的数字小于等于4时:偶数则舍5进1;奇数直接舍去; + 5前端的数字大于4时:舍5进1。 所以说,我们使用 `toFixed` 的方案来解决浮点数的运算问题,是不安全的。 ## 为什么乘以高倍数后再运算不安全? 我们先看一下十进制下乘法运算是怎么做的 ``` 0.1 * 0.2 0.1 * 0.2 ------ = 0.02 1.12 * 2.2 1.12 * 2.20 ------ 0 224 224 ------ 2.4640 也可以这样子 1.12 * 2.20 ------ =2.4640 ``` Q:16.1 * 1000 === 16100? ``` 16.1 => 10000.0001100110011001100110011001100110011001100110011001... => 1.0000000110011001100110011001100110011001100110011010 * 2^10000000011 1000 => 1111101000.000000... => 1.1111010000000000000000000000000000000000000000000000 * 2^10000001000 ``` 阶码相加: ``` 10000000011 +10000001000 ------------ =100000001011 2059 - 1023 = 1036 = 10000001100 ``` 尾数相乘: ``` 1.000000011001100110011001100110011001100110011001101|0 1.111101|00000000000... ----------------------------------------------------------------------------- 0000001000000011001100110011001100110011001100110011001101 0000000000000000000000000000000000000000000000000000000000 00001000000011001100110011001100110011001100110011001101 0001000000011001100110011001100110011001100110011001101 001000000011001100110011001100110011001100110011001101 01000000011001100110011001100110011001100110011001101 1000000011001100110011001100110011001100110011001101 ----------------------------------------------------------------------------- ===> 0000001000000011001100110011001100110011001100110011001101 0000000000000000000000000000000000000000000000000000000000 +0000100000001100110011001100110011001100110011001100110100 ----------------------------------------------------------- =0000101000010000000000000000000000000000000000000000000001 +0001000000011001100110011001100110011001100110011001101000 +0010000000110011001100110011001100110011001100110011010000 ----------------------------------------------------------- =0011000001001100110011001100110011001100110011001100111000 +0100000001100110011001100110011001100110011001100110100000 +1000000011001100110011001100110011001100110011001101000000 ----------------------------------------------------------- =1100000100110011001100110011001100110011001100110011100000 ----------------------------------------------------------------------------- ===> 0000101000010000000000000000000000000000000000000000000001 +0011000001001100110011001100110011001100110011001100111000 ----------------------------------------------------------- =0011101001011100110011001100110011001100110011001100111001 +1100000100110011001100110011001100110011001100110011100000 ----------------------------------------------------------------------------- ===> 0011101001011100110011001100110011001100110011001100111001 +1100000100110011001100110011001100110011001100110011100000 ----------------------------------------------------------- =1111101110010000000000000000000000000000000000000000011001 最终运算出来的结果是 1.111101110010000000000000000000000000000000000000000011001 * 2^10000001100 截取之后等于 1.1111011100100000000000000000000000000000000000000001 * 2^10000001100 换算成十进制 11111011100100.000000000000000000000000000000000000001 * 2^0 =16100.000000000002 ``` 通过上面的运算,我们发现乘以一个更大的倍数后再计算也是不安全的。 ## 最终解决方案 所以需要比较精确的浮点数运算,我还是建议使用一些现有的库来完成计算。例如:[decimal.js](https://www.npmjs.com/package/decimal) ```js import Decimal from 'decimal.js' const a = new Decimal(0.1) const b = a.add(0.2) console.log(b.toNumber()) // 0.3 const c = new Decimal(16.1) const d = c.mul(1000) console.log(d.toNumber()) // 16100 ``` ## 附 ### Node中浮点数转二进制字符方法 ```js /** * floatToBinStr * 浮点数转成二进制字符 * @param {number} input */ function floatToBinStr (input) { if (typeof input !== 'number') { throw new TypeError('parameter `input` must be a number type.') } const buff = Buffer.allocUnsafe(8) buff.writeDoubleBE(input) let binStr = '' for (let i = 0; i < 8; i++) { binStr += buff[i].toString(2).padStart(8, '0') } return { output: binStr, // 符号 0 => 正;1 => 负 sign: binStr[0], // 指数 exponent: binStr.slice(1, 12), // 有效尾数 mantissa: binStr.slice(12), // 科学计数法 scientificNotation: `(-1)^${binStr[0]}*1.${binStr.slice(12)}*2^${Number(`0b${binStr.slice(1, 12)}`) - 1023}` } } [0.1, 0.2, 0.3, 0.4, 16.1, 1000.0].forEach(number => { console.log(`----- ${number} ---------------`) const { output, sign, exponent, mantissa, scientificNotation } = floatToBinStr(number) console.log(`输出: ${output}`) console.log(`符号: ${sign}`) console.log(`指数: ${exponent}`) console.log(`有效尾数: ${mantissa}`) console.log(`科学计数法: ${scientificNotation}`) console.log('-------------------------------') }) ``` ### 浮点数调用 toString(2) 为什么返回的结果长度不一样? ![image-20220509180120323.png](https://img-squad-prod.humandetail.com/inner/20220509KitsIi6T.png) 为什么呢? 我们先看下这几个数存储的内容是什么: ``` 0.1 => 0|01111111011|1001100110011001100110011001100110011001100110011010 0.2 => 0|01111111100|1001100110011001100110011001100110011001100110011010 0.3 => 0|01111111101|0011001100110011001100110011001100110011001100110011 0.4 => 0|01111111101|1001100110011001100110011001100110011001100110011010 ``` JS在转成字符串时,会将阶数变成 0,转换完成后会舍弃后面为0的位,然后再输出: ``` 0.1 => 1.1001100110011001100110011001100110011001100110011010 * 2^01111111011 => 0.00011001100110011001100110011001100110011001100110011010 * 2^0 => '0.00011001100110011001100110011001100110011001100110011010' // 舍弃后面无用的0 => '0.0001100110011001100110011001100110011001100110011001101' length: 57 0.2 => 1.1001100110011001100110011001100110011001100110011010 * 2^01111111100 => 0.0011001100110011001100110011001100110011001100110011010 * 2^0 => '0.0011001100110011001100110011001100110011001100110011010' // 舍弃后面无用的0 => '0.001100110011001100110011001100110011001100110011001101' length: 56 0.3 => 1.0011001100110011001100110011001100110011001100110011 * 2^01111111101 => 0.010011001100110011001100110011001100110011001100110011 * 2^0 => '0.010011001100110011001100110011001100110011001100110011' length: 56 0.4 => 1.1001100110011001100110011001100110011001100110011010 * 2^01111111101 => 0.011001100110011001100110011001100110011001100110011010 * 2^0 => '0.011001100110011001100110011001100110011001100110011010' // 舍弃后面无用的0 => '0.01100110011001100110011001100110011001100110011001101' length: 55 ```
Vite动态路由和异步组件我当业余面试官的那些事儿

推荐文章

  1. CentOS8安装Web相关服务

    CentOS8安装Web相关服务

  2. 灵魂拷问:0.1+0.2===0.3吗?

    灵魂拷问:0.1+0.2===0.3吗?

  3. Vue3.0初体验(Composition API)

    Vue3.0初体验(Composition API)

  4. Event Loop 我感觉我又行了(浏览器篇)

    Event Loop 我感觉我又行了(浏览器篇)

  5. 观察者模式 VS 发布订阅者模式

    观察者模式 VS 发布订阅者模式

标签归档

  • monorepo (1)
  • pnpm (1)
  • JSDoc (1)
  • 手写源码 (1)
  • Event Loop (2)
  • Vite (1)
  • Linux (1)
  • nuxt (2)
  • axios (2)
  • 设计模式 (2)
  • markdown (1)
  • vue3.0 (2)
  • element-ui (2)
  • React (1)
  • 页面渲染 (2)
  • TypeScript (9)
  • webpack (3)
  • Git (2)
  • canvas (3)
  • 网络 (7)
  • ES6 (27)
  • VueJS (9)
  • 事件 (3)
  • HTML (2)
  • 布局技巧 (4)
  • 用户体验 (2)
  • 性能优化 (1)
  • 火狐插件 (1)
  • 微信支付 (1)
  • ThinkPHP (2)
  • 移动端 (2)
  • cookie (1)
  • 算法 (1)
  • 富文本编辑器 (2)
  • 面向对象 (3)
  • jQuery (3)
  • 递归 (1)
  • JavaScript (65)
  • 正则表达式 (2)
  • Apache (1)
  • MySQL (8)
  • CSS (12)
  • PHP (11)
© 2014 - 2023 Humandetail. All Rights Reserved.
粤ICP备14074910号 粤公网安备44010602007739