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

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



相关文章
|
1月前
|
前端开发 JavaScript 算法
【面试题】 JavaScript 中的深浅拷贝: 原理与实现
【面试题】 JavaScript 中的深浅拷贝: 原理与实现
|
1月前
|
JavaScript 数据可视化
基于fabric.js的图片编辑器, 画布背景实现原理
基于vue3 + fabric.js + vite + element-plus + typescript等技术,画布背景原理分析
基于fabric.js的图片编辑器, 画布背景实现原理
|
1月前
|
存储 前端开发 JavaScript
【JavaScript】浮点数精度问题
浮点数精度问题是指在计算机中使用二进制表示浮点数时,由于二进制无法精确表示某些十进制小数,导致计算结果可能存在舍入误差或不精确的情况。 这个问题主要源于浮点数的存储方式。在计算机中,浮点数通常使用IEEE 754标准来表示。该标准将浮点数分为符号位、指数位和尾数位,使用科学计数法来表示一个浮点数。
33 0
|
1月前
|
Web App开发 JavaScript 前端开发
Node.js 的事件循环原理、工作流程
Node.js 的事件循环原理、工作流程
26 0
|
2月前
|
负载均衡 JavaScript 算法
Node.js 多进程的概念、原理、优势以及如何使用多进程来提高应用程序的性能和可伸缩性
Node.js 多进程的概念、原理、优势以及如何使用多进程来提高应用程序的性能和可伸缩性
27 1
|
5天前
|
JavaScript 前端开发 开发者
深入探讨前端框架Vue.js的数据绑定原理
在前端开发中,数据绑定是Vue.js框架的核心特性之一,它实现了视图与数据的双向绑定,极大地提高了开发效率和用户体验。本文将深入探讨Vue.js数据绑定的原理,从响应式数据、依赖追踪到虚拟DOM等方面进行详细分析,帮助读者更好地理解Vue.js框架的工作机制。
9 0
|
6天前
|
存储 缓存 JavaScript
解密前端框架Vue.js的响应式原理
作为当下最流行的前端框架之一,Vue.js的响应式原理是其核心之一。本文将深入探讨Vue.js的响应式原理,从数据劫持、依赖收集到更新视图的完整流程,帮助读者更好地理解Vue.js框架的工作方式。
|
24天前
|
JavaScript 前端开发 开发者
深入理解前端框架Vue.js的数据响应式原理
本文将深入探讨Vue.js前端框架中的数据响应式原理,包括双向绑定、依赖追踪和虚拟DOM等核心概念。通过详细解析Vue.js内部实现机制,读者能够更好地理解其工作原理,并在实际开发中更灵活地运用。
|
25天前
|
JavaScript 前端开发 算法
探究前端框架Vue.js的响应式原理
本文将深入探讨前端框架Vue.js的核心特性——响应式原理。我们将介绍Vue.js中的数据绑定、依赖追踪和虚拟DOM等概念,并通过具体的示例代码解析其工作机制。通过了解Vue.js的响应式原理,开发者可以更好地利用这一特性构建灵活、高效的前端应用。
|
27天前
|
存储 JavaScript 前端开发
JavaScript 自定义对象 及 new()原理与实现 如何完整地手写实现new
JavaScript 自定义对象 及 new()原理与实现 如何完整地手写实现new
35 0

相关产品

  • 云迁移中心