js浮点数存储精度丢失原理

简介: 而我们也得出一个原因,因为精度丢失所致。下面我将一步一步地以最简单的0.1为例告诉你们精度为什么丢失,什么时候开始丢失的,这里没有深奥的公式,也没有晦涩的概念,只要你知道进制转换就能看懂了。

前言

曾几何时我们惊讶于在控制台看到这样的情况

0.1 + 0.2 === 0.3
false
复制代码

而我们也得出一个原因,因为精度丢失所致。下面我将一步一步地以最简单的0.1为例告诉你们精度为什么丢失,什么时候开始丢失的,这里没有深奥的公式,也没有晦涩的概念,只要你知道进制转换就能看懂了。

0.1在内存中的样子

有一点我们是知道的,js中一般的数值是以64位浮点数存储在内存中的,也就是这64个二进制数字映射着一个具体的数字,具体是按照IEEE754 这个标准来的,这个标准权衡了精度和表示范围,也就是如何有效利用这64个二进制数字的前提下提出的。下面的所有流程都是按这个标准来的,其中把64位划分出了3个区域

区域 S 符号位 用 1 位表示 0表示正数 1表示负数

区域 E 指数位 用 11 位表示 有正负范围,临界值是1023 后面看转换过程就能看明白

区域 M 尾数位 用 52 位表示

S + E + M 刚好就等于64位 在开始前先看看 0.1 在内存中是长什么样子的

let bytes = new Float64Array(1);// 64位浮点数
bytes[0] = 0.1;// 填充0.1进去
let view = new DataView(bytes.buffer);
console.log(view.getUint8(0).toString(2));// 10011010
console.log(view.getUint8(1).toString(2));// 10011001
console.log(view.getUint8(2).toString(2));// 10011001
console.log(view.getUint8(3).toString(2));// 10011001
console.log(view.getUint8(4).toString(2));// 10011001
console.log(view.getUint8(5).toString(2));// 10011001
console.log(view.getUint8(6).toString(2));// 10111001
console.log(view.getUint8(7).toString(2));// 00111111 这里补齐了8位
复制代码

这里的bytes.buffer代表的就是一串内存空间,为了方便大家理解我使用 DataView用无符号8位的格式一个一个地读取内存的数据再转为二进制格式。 由于读取内存的顺序会受字节序的影响,可能在你们的电脑打印得到相反的顺序 如果按SEM的排列,那么其二进制就像下面这样子的

s(0)E(01111111011)M(1001100110011001100110011001100110011001100110011010)

现在已经知道了0.1在内存的样子,下面就开始说说具体的转化过程,也就是精度丢失的过程

0.1精度丢失过程

  1. 转换为二进制
    在转换之前,首先看十进制小数要如何转化为二进制数小数的,这也是理解精度丢失十分关键的步骤,这个网上也有很多资料,我下面简单写一下流程。
0.1 => 0.2 => 0.4 => 0.8 => 1.6 => 1.2 => 0.4 => 0.8 => 1.6 => 1.2 => 0.4 => 0.8 => 1.6 => 1.2 => 0.4 ..............
复制代码

就是小数部分不断乘以2,并取整数部分的值,直到小数部分为0为止,应该也是很好理解的,可以看出这样下去是一个无限循环的过程,转化后是这样子的

0.00011001100110011001100110011001100110011001100110011001100110011001.....
复制代码

有限空间传入无限的数很明显是不可能,那么应该怎么做呢

  1. 转换为二进制指数格式

    转换为指数格式其实就是移动小数点,让小数点前面出现的是第一个为1的值,不同的二进制数据,可能是前移可能是右移,对应的是指数的正负范围,转换后是这样子的

1.1001100110011001100110011001100110011001100110011001100110011001..... * 2 ^ -4
复制代码
  1. 提取数据,进行数值截取,导致精度丢失

    这里可以看到向右移动了4位,这个数据会保存在指数区域E内,在没有移位的情况下指数区域的值是1023,向左移动几位就加几位,向右移动几位就减几位,所以这里是

1023 - 4 = 1019
1019 转二进制并补齐11位  01111111011
复制代码

也就是E为 01111111011 由于尾数位最多只有52位,所以小数点后面的52位全部提取到尾数位,其中要注意的是,类似四舍五入,如果末位后是1会产生进位,这里就产生了进位

1001100110011001100110011001100110011001100110011001100110011001.....
1001100110011001100110011001100110011001100110011001 100110011001.....
进位后截取
1001100110011001100110011001100110011001100110011010
复制代码

也就是M为 1001100110011001100110011001100110011001100110011010

这里由于丢掉了部分数据,所以导致精度丢失

由于0.1是正数,所以 S 为 0

到此整个js浮点数存储过程就结束了,为了表示我不是忽悠大家的,大家可以对照第一部分输出的数据值。下面将顺便介绍一下怎么转回十进制

丢失精度的数据转回十进制

  1. 提取尾数位数据
1001100110011001100110011001100110011001100110011010
复制代码
  1. 先前添加 1. 恢复为指数格式 并提取指数位
1.1001100110011001100110011001100110011001100110011010
复制代码
01111111011 => 1019
1019 - 1023 = -4
复制代码
1.1001100110011001100110011001100110011001100110011010 * 2 ^ -4
复制代码
  1. 移位
0.00011001100110011001100110011001100110011001100110011010
复制代码
  1. 二进制转化为十进制 小数的二进制转化为十进制网上的资料也有很多,我也简单介绍一下过程,以0.0111为例子
 0.0111 小数点后一位 0 / 2^1   0
        小数点后2位 1 / 2^2    0.25
        小数点后3位 1 / 2^3    0.125
        小数点后4位 1 / 2^4    0.0625
        然后相加 0 + 0.25 + 0.125 + 0.0625 = 0.4375
复制代码

按以上方法进行装换

0.00011001100110011001100110011001100110011001100110011010 =>
0.100000000000000005551
复制代码

关于最后这个输出值其实也是不精确的,因为我就是用js计算的,如果大家有更准确的计算方法可以帮我算一下,精确的值末尾数应该是5才对。但是你试一下在控制台中计算下面的表达式

0.1.toPrecision(21)
"0.100000000000000005551"
复制代码

这个也证明了上述的推理过程是正确的

总结

相信到这里你已经知道为什么精度会丢失了,很多人都说js做浮点数计算很坑,其实也只是遵守标准而已,如果是坑的话,这个坑就不止是js了。



原文发布时间为:2018年06月30日

本文作者:changli2018

本文来源:掘金 如需转载请联系原作者



相关文章
|
17天前
|
JavaScript 前端开发
JS浮点数精度问题及高精度小数运算:BigNumber解决方案
JS浮点数精度问题及高精度小数运算:BigNumber解决方案
71 0
|
17天前
|
自然语言处理 JavaScript 前端开发
深入理解JavaScript中的闭包:原理与实战
【10月更文挑战第12天】深入理解JavaScript中的闭包:原理与实战
|
17天前
|
前端开发 JavaScript
深入理解JavaScript中的事件循环(Event Loop):从原理到实践
【10月更文挑战第12天】 深入理解JavaScript中的事件循环(Event Loop):从原理到实践
30 1
|
26天前
|
数据采集 JavaScript 前端开发
JavaScript逆向爬虫——无限debugger的原理与绕过
JavaScript逆向爬虫——无限debugger的原理与绕过
35 2
|
17天前
|
自然语言处理 JavaScript 前端开发
深入理解JavaScript中的闭包:原理、应用与代码演示
【10月更文挑战第12天】深入理解JavaScript中的闭包:原理、应用与代码演示
|
18天前
|
自然语言处理 JavaScript 前端开发
深入理解JavaScript闭包:原理与应用
【10月更文挑战第11天】深入理解JavaScript闭包:原理与应用
14 0
|
21天前
|
JavaScript 前端开发 开发者
深入理解JavaScript中的闭包:原理与应用
【10月更文挑战第8天】深入理解JavaScript中的闭包:原理与应用
|
2月前
|
前端开发 JavaScript Java
JavaScript的运行原理
JavaScript 的运行原理包括代码输入、解析、编译、执行、内存管理和与浏览器交互几个步骤。当打开网页时,浏览器加载 HTML、CSS 和 JavaScript 文件,并通过 JavaScript 引擎将其解析为抽象语法树(AST)。接着,引擎将 AST 编译成字节码或机器码,并在执行阶段利用事件循环机制处理异步操作,确保单线程的 JavaScript 能够高效运行。同时,JavaScript 引擎还负责内存管理和垃圾回收,以减少内存泄漏。通过与 DOM 的交互,JavaScript 实现了动态网页效果,提供了灵活且高效的开发体验。
|
2月前
|
存储 JavaScript 前端开发
[JS] ES Modules的运作原理
【9月更文挑战第16天】ES Modules(ECMAScript Modules)是 JavaScript 中的一种模块化开发规范,适用于浏览器和 Node.js 环境。它通过 `export` 和 `import` 关键字实现模块的导出与导入。模块定义清晰,便于维护和测试。JavaScript 引擎会在执行前进行静态分析,确保模块按需加载,并处理循环依赖。ES Modules 支持静态类型检查,现代浏览器已原生支持,还提供动态导入功能,增强了代码的灵活性和性能。这一规范显著提升了代码的组织和管理效率。
|
3月前
|
缓存 JavaScript 前端开发
[译] Vue.js 内部原理浅析
[译] Vue.js 内部原理浅析