JS中如何理解浮点数?
本文由云+社区发表
相信大家在平常的 JavaScript 开发中,都有遇到过浮点数运算精度误差的问题,比如 console.log(0.1+0.2===0.3)// false。在 JavaScript 中,所有的数字包括整数和小数都是用 Number 类型来表示的。本文通过介绍 Number 的二进制存储标准来理解浮点数运算精度问题,和理解 Number 对象的 MAX_VALUE 等属性值是如何取值的,最后介绍了一些常用的浮点数精度运算解决方案。
Number 的存储标准
JavaScript Number 采用的是 IEEE 754 定义的 64 位双精度浮点型来表示。具体的字节分配可以先看一下引自维基百科的图:

从上图中可以看到,从高到低,64位被分成3段,分别是:
- sign: 符号位,占 1 位;
- exponent: 指数位,占 11 位;
- fraction: 有效数字位,占 52 位。
指数位有 11 位,取值范围是 0 到 2047。当指数位 e=0 或者 e=2017 时,根据有效数字位 f 是否为 0 ,具有不同的特殊含义,具体见下表:

对于常用的 normal number, 为了方便表示指数为负数的情况,所以,指数位数值大小做了一个 -1023 的偏移量。对于一个非 0 数字而言,,它的二进制的科学计数法里的第一位有效数字固定是 1。这样,一个双精度浮点型数字的值就是

对于 subnormal number,它可以用来表示更加接近于 0 的数,它特殊的地方是有效数字位的前面补充的是 0 而不是 1,且指数为偏移量是 -1022,所以值是:

Number 对象中的几个属性值
知道了 Number 是如何存储之后,Number 对象的属性是如何取值的就明朗了。
Number.MAX_VALUE:可表示的最大的数,显然 e 和 f 都取最大时能表示的数最大,值为

Number.MIN_VALUE:可表示的最小的正数,用最小的 subnormal number 来表示。当 e = 0 ,f 的最后一位为 1,其他为 0 时最小,值为

Number.EPSILON : 表示 1 与 Number 可表示的大于 1 的最小的浮点数之间的差值。值为

Number.MAXSAFEINTEGER:表示在 JavaScript 中最大的安全整数。可以连续且精确被表示出来的整数成为安全整数,比如 2^54 就不是个安全整数,因为它和 2^54+1 两个数的表示是完全一样的,e=1077,f=0。 Math.pow(2,54)===Math.pow(2,54)+1// true。整数转化为二进制后,小数点后是不会有数字的,而用二进制的科学计数法表示时,小数点后最多保留 52 位,加上前置的一个 1,有 53 位数字,所以当一个数转化二进制时,如果位数超过 53 位,必然会截断末尾的部分,即导致不能精确表示,即为不安全整数。所以最小的会被截断的整数是 100...001=2^53+1(中间有52个0)。这个数设为 X,则比 X 小的整数都能被精确表示出来,再加上“连续”这个条件,所以 X-1 不是我们要的答案,X-2 才是。 Number.MAX_SAFE_INTEGER 最终值为

Number.MINSAFEINTEGER:表示在 JavaScript 中最小的安全整数,对 Number.MAX_SAFE_INTEGER 取负值即可,值为 -9007199254740991
为什么0.1+0.2不等于0.3
现在看看 console.log(0.1+0.2===0.3)// false 这个问题,数字 0.1 转化成二进制是 0.0001100110011... 即 1.10011001...1001 * 2^-4 (小数部分有52位,即有13个1001循环)。由于第 53 位是 1,类似 10 进制的四舍五入,二进制是“零舍一入”,所以 0.1 的最终二进制科学计数法表示是 1.10011001...1010 * 2^-4,即二进制数值大小实际上是 0.000110011001...10011010。下面的代码验证了这个值(打印出来的值,把最末尾的0去掉了):
var a = 0.1;console.log(a.toString(2)); //0.0001100110011001100110011001100110011001100110011001101
同理十进制数字 0.2 转化为二进制的最终值是 1.10011001...1010 * 2^-3 即 0.00110011...100111010;十进制 0.3 转化位二进制的最终值是 1.00110011...0011 * 2^-2
var b = 0.2;console.log(b.toString(2)); //0.001100110011001100110011001100110011001100110011001101var c = 0.3;console.log(c.toString(2)); //0.010011001100110011001100110011001100110011001100110011
所以,0.1+0.2 的值即为上面 0.1 和 0.2 对应的二进制数值的相加,如下图所示

上图中,对所得的和,“零舍一入”保留 52 位有效小数就是最终的值:0.01001100...110100(第 53 位是 1 ,所以往前进了 1),如下代码所示。这个值与上文中的 0.3 的最终二进制表示的值明显不相同,即解释了 0.1 + 0.2 不等于 0.3 的根本原因所在(实际上,这个值转化为 10 进制约等于 0.30000000000000004)。注:打印出来的长度是 54,因为有 52 位有效小数,前面是'0.01',长度是 4,最后去掉末尾的 2 个 0,所以最后打印出来的长度是 52+4-2 = 54。
var d = 0.1 + 0.2;console.log(d.toString(2)); //0.0100110011001100110011001100110011001100110011001101console.log(d.toString(2).length); // 54
浮点数精度运算解决方案
关于 js 浮点数运算精度丢失的问题,不同场景可以有不同的解决方案。 1、如果只是用来展示一个浮点数的结果,则可以借用 Number 对象的 toFixed 和 parseFloat 方法。下面代码片段中,fixed 参数表示要保留几位小数,可以根据实际场景调整精度。
function formatNum(num, fixed = 10) { return parseFloat(a.toFixed(fixed))}var a = 0.1 + 0.2;console.log(formatNum(a)); //0.3
2、如果需要进行浮点数的加减乘除等运算,由上文可知,在小于 Number.MAXSAFEINTEGER 范围的整数是可以被精确表示出来的,所以可以先把小数转化为整数,运算得到结果后再转化为对应的小数。比如两个浮点数的加法:
function add(num1, num2) { var decimalLen1 = (num1.toString().split('.')[1] || '').length; //第一个参数的小数个数 var decimalLen2 = (num2.toString().split('.')[1] || '').length; //第二个参数的小数个数 var baseNum = Math.pow(10, Math.max(decimalLen1, decimalLen2)); return (num1 * baseNum + num2 * baseNum) / baseNum;}console.log(add(0.1 , 0.2)); //0.3
参考资料
- https://en.wikipedia.org/wiki/IEEE_754
- https://en.wikipedia.org/wiki/Double-precision_floating-point_format
- https://en.wikipedia.org/wiki/Normal_number_(computing)
- https://en.wikipedia.org/wiki/Denormal_number
此文已由作者授权腾讯云+社区发布
JS中如何理解浮点数?的更多相关文章
- 【转】JS中处理Number浮点数精度问题
https://github.com/dt-fe/number-precision ~(function(root, factory) { if (typeof define === "fu ...
- JS中call和apply区别有哪些 记录
一.call和apply区别 传递参数的方式.用法上不同,主要是参数不完全同 (1).B.Function.call(A,arg,arg) 这个例子是讲A对象“调用”B对象的Function(某个具体 ...
- 怎么理解js中的事件委托
怎么理解js中的事件委托 时间 2015-01-15 00:59:59 SegmentFault 原文 http://segmentfault.com/blog/sunchengli/119000 ...
- 彻底理解js中this的指向,不必硬背。
首先必须要说的是,this的指向在函数定义的时候是确定不了的,只有函数执行的时候才能确定this到底指向谁,实际上this的最终指向的是那个调用它的对象(这句话有些问题,后面会解释为什么会有问题,虽然 ...
- 关于js中闭包的理解
1.以前很不理解js中闭包的概念及使用,下面来看一下 function foo() { var a = 123; var b = 456; return function () { return a; ...
- js笔记——理解js中的call及apply
call及apply在js里经常碰得到,但一直感觉很陌生,不能熟练使用.怎样才能熟练应用呢? 为什么存在call和apply? 在javascript OOP中,我们经常会这样定义: function ...
- js中的回调函数的理解和使用方法
js中的回调函数的理解和使用方法 一. 回调函数的作用 js代码会至上而下一条线执行下去,但是有时候我们需要等到一个操作结束之后再进行下一个操作,这时候就需要用到回调函数. 二. 回调函数的解释 因为 ...
- 理解js中的自由变量以及作用域的进阶
如果你不知道什么是作用域,建议你先看什么是作用域链,什么是原型链.这篇文章,因为这些内容都是有关联性的. 什么是自由变量? 如我在全局中定义了一个变量a,然后我在函数中使用了这个a,这个a就可以称之为 ...
- js中的闭包之我理解
闭包是一个比较抽象的概念,尤其是对js新手来说.书上的解释实在是比较晦涩,对我来说也是一样. 但是他也是js能力提升中无法绕过的一环,几乎每次面试必问的问题,因为在回答的时候.你的答案的深度,对术语的 ...
随机推荐
- js 对象与json的转化
1.将对象转换为JSON格式字符串 JSON.stringify(object) 2.将JSON字符串转换为对象 JSON.parse(jsonString);
- Codeforces Round #548 (Div. 2) E 二分图匹配(新坑) or 网络流 + 反向处理
https://codeforces.com/contest/1139/problem/E 题意 有n个学生,m个社团,每个学生有一个\(p_i\)值,然后每个学生属于\(c_i\)社团, 有d天,每 ...
- vue中引入vux
1.安装相关依赖 cnpm install vux --save cnpm install vux-loader --save-dev cnpm install less less-loader -- ...
- bootstrap-datepicker简单使用
粗略整理,可能存在其他的方式请大家多多指教 选择年份 html <div class="dropdown"> <label class="search- ...
- Docker pull下来的镜像(2)
1.通过docker pull ubuntu:12.04 命令拉取镜像,ll发现当前目录并没有什么变化. 2.切换到docker目录 [root@iZwz9fedjw2xvy6fvxfnxgZ zxy ...
- Filter的使用(web作业)
一:什么是过滤器 Filter:Servlet过滤器Fileter是一个小型的web组件,它们通过拦截请求和响应,以便查看.提取或以某种方式操作客户端和服务器之间交换的数据,实现“过滤”的功能.Fil ...
- 2019浙大校赛--G--Postman(简单思维题)
一个思维水题 题目大意为,一个邮递员要投递N封信,一次从邮局来回只能投递K封.求最短的投递总距离.需注意,最后一次投递后无需返回邮局. 本题思路要点: 1.最后一次投递无需返回邮局,故最后一次投递所行 ...
- mvc 路由伪静态实现
很多网站都采用伪静态,例如以html.shtml等结尾的url,mvc的路由可以轻松实现. 配置路由 默认路由配置 添加伪静态路由 mvc的路由原理是从上往下匹配的,所以只需要在后面添加自己配置的路由 ...
- hightopo自己用开源的方案重构一遍
经过一年多的学习吧前面路上的坑基本算踩过一遍了 所以下面计划吧hightopo网站上的demo用自己的方式重新写一遍
- 更改了ssh文件下,还没有权限
执行 cd /root/.ssh chmod 600 id_*