可能很多小伙伴都忘记了js的Number对象有一个保留小数位数的方法:toFixed();传入一个需要保留的位数就OK:
因为toFixed方法返回的是一个字符串,所以别忘了把字符串转回浮点数
计算机内部如何表示数
我们都知道,计算机用位来储存及处理数据。每一个二进制数(二进制串)都一一对应一个十进制数。
这里以十进制数13来展示“按位计数法”如何表示整数:
十进制值进制 按位格式描述
13 10 13 1x10^1 + 3x10^0 = 10 + 3
13 2 11011x2^3 + 1x2^2 + 0x2^1 + 1x2^0 = 8 + 4 + 0 + 1
十进制值进制 按位格式描述
0.625 10 0.625 6x10^-1 + 2x10^-2 + 5x10^-3 = 0.6 + 0.02 + 0.005
0.625 2 0.101 1x2^-1 + 0 x2^-2 + 1x2^-3 = 1/2 + 0 + 1/8
十进制整数转二进制方法:除2取余;十进制小数转二进制方法:乘2除整
十进制0.1转换成二进制,乘2取整过程:
0.1 * 2 = 0.2 # 0
0.2 * 2 = 0.4 # 0
0.4 * 2 = 0.8 # 0
0.8 * 2 = 1.6 # 1
0.6 * 2 = 1.2 # 1
0.2 * 2 = 0.4 # 0
从上面可以看出,0.1的二进制格式是:0.0001100011....。这是一个二进制无限循环小数,但计算机内存有限,我们不能用储存所有的小数位数。那么在精度与内存间如何取舍呢?
有误差的两个数,其计算的结果,当然就很可能与我们期望的不一样了。注意前面的这句话中的“很可能”这三个字?为啥是很可能昵?
0.1 + 0.1 为什么等于0.2
答案是:两个有舍入误差的值在求和时,相互抵消了,但这种“负负得正,相互抵消”不一定是可靠的,当这两个数字是用不同长度数位来表示的浮点数时,舍入误差可能不会相互抵消。
又如,对于 0.1 + 0.3 ,结果其实并不是0.4,但0.4是最接近真实结果的数,比其它任何浮点数都更接近。许多语言也就直接显示结果为0.4了,而不展示一个浮点数的真实结果了。
另外要注意,二进制能精确地表示位数有限且分母是2的倍数的小数,比如0.5,0.5在计算机内部就没有舍入误差。所以0.5 + 0.5 === 1
计算机这样胡乱舍入,能满足所有的计算需求吗
我们看两个现实的场景:
对于一个修建铁路的工程师而言,10米宽,还是10.0001米宽并没有什么不同。铁路工程师就不需要这么高0.x这样的精度
对于芯片设计师,0.0001米就会是一个巨大不同,他也永远不用处理超过0.1米距离
不同行业,要求的精度不是线性的,我们允许(对结果无关紧要的)误差存在。10.0001与10.001在铁路工程师看来都是合格的。
虽然允许误差存在,但程序员在使用浮点数进行计算或逻辑处理时,不注意,就可能出问题。记住,永远不要直接比较两个浮点的大小:
这一篇真的要说0.1 + 0.2 了。。
先来一个计算题来热热身吧。
我知道你想怎么做:362500 + 314 = 362814。没错,是这样算的,再来一个。
还用刚才的办法吗?还是算了吧,实在太长了,换一种办法:
运算法则不用多说了吧,这种方法还是很简单的,IEEE754浮点数也是使用这种办法计算的加法的,准确来说,是计算机中所有种类的浮点数都是这么计算加法的,因为浮点数标准可不只有IEEE一家才有。
现在借用工具把 0.1 和 0.2 的 64位浮点数格式表示出来。
图中上面的64位浮点数代表0.1,下面的64位浮点数代表0.2( 注意,因为计算时肯定是拿的内存中的值,所以此时的0.1 和 0.2 都是经过存储前舍入处理的了 ),并且已经列出了加法计算的5个步骤:
一. 阶码对阶
二. 尾数相加
三. 结果规格化
四. 尾数舍入处理
五. 溢出判断
那就按照顺序来一一说明。
首先,要把指数位(阶码)调整成一致才能计算加法,但是和开篇的计算题不同的是,这里规定必须是 小阶向大阶看齐 。
很明显,0.1的阶码比0.2的要小,所以0.1的尾数(小数位)向右移动一位,同时阶码 + 1。
点击下一步后如下所示:
因为阶码部分暂时已经确定,所以拿出来单独显示。
这一步比较简单,就是使用加法器把两个值相加,结果如下图所示。
还记得规格化是什么东西吗,联想一下科学计数法,计算后的尾数需要变成1.xxxx的形式,通过尾数的右移动来实现规格化,整体右移一位,阶码 +1。本案例中需要右移一位即可,所以阶码 +1。
舍入规则同第四章所说一致,可以返回 《JS基础-数字-0.1+0.2!=0.3(四)》 查看,当前命中了五取偶。
由于符合舍入规则五取偶中的 “如果X的值为1,则进1”,所以进1
溢出判断规则:
本案例没有命中溢出,所以不需要处理。
对比一下真正的0.3存入内存时的64位浮点数是什么样的:
细心的同学应该发现了其中的不同,0.1 + 0.2 的最终结果和 0.3 相比,在最后一位多进了一个1。这也就是为什么他们不相等的原因,0.1 + 0.2 已经不是 0.3 了。
首先,每一个IEEE754浮点编码都必须对应一个唯一的10进制的值,这是必须的,因为IEEE754没有规定每一个编码转化成的10进制的值必须是多少,但是却规定, 一个IEEE754浮点编码转化成10进制的值不做绝对限制,但当这个10进制的值在转化回IEEE754浮点编码时,必须还能还原成之前的IEEE754浮点编码。
如果 IEEE754浮点编码 0011111111010011001100110011001100110011001100110011001100110011 和 0011111111010011001100110011001100110011001100110011001100110100 都代表了0.3,那么当我存储0.3时,要转化成这两个编码的哪一个呢?
其次,如果如你所想的这么干了,0.3 占用了2个编码,其他的不精准情况是不是也要占用多个编码啊?(比如: 0.2 + 0.4 = 0.6000000000000001 等等)这种情况很常见,都这么干, IEEE754浮点编码一共才拥有 18437736874454810627(即,264 - 253 + 3)个值,将会有多少无效的编码掺杂其中。
最后,如果有个JS开发人员就想存 0.30000000000000004 这个值,为啥不让人家存啊, 0.30000000000000004 招谁惹谁了,它也是IEEE754 64位浮点编码的合法公民啊。
无论舍入界限值设置为多少,总会有这个值附近的倒霉蛋赶上。所以我现在只能使用《你不知道的JavaScript (中卷)》19页的最上面的话回复你:
看来我们能做的只有了解它,并且小心使用。
请不要以JavaScript的数值精度问题为借口来贬低这门语言,如果你这么做了,只能说明你还不懂爱情。
作为开发人员,针对小数的存储和运算问题不能怪罪IEEE754,因为误差不是IEEE754特有的,而是二进制浮点数表示方法引起的。怪罪2进制浮点数也不合适,因为计算机是2进制的。计算机也很冤枉,因为科学已经证实自然常数E(约等于2.718281828459045)进制的计算机执行效率最高,与之最近的只能是2和3,2进制计算机的物理原件相比于3进制更容易制造。即便普及的是3进制计算机,10进制小数依然无法完全转化。
如果你非要找个替罪羔羊泄愤,那只能拿锤子砸自己的手了,因为如果人类生来就只有8个手指头,习惯于使用8进制,转化成2进制小数就不会有转不尽的情况了。
以上全部,仅代表个人观点。
js中的数字都是用浮点数表示的,并规定使用IEEE 754 标准的双精度浮点数表示。
IEEE 754 规定了两种基本浮点格式:单精度和双精度。
IEEE单精度格式具有24 位有效数字精度(包含符号号),并总共占用32 位。
IEEE双精度格式具有53 位有效数字精度(包含符号号),并总共占用64 位。
十进制0.1=> 二进制0.00011001100110011…(循环0011)
=>尾数为1.1001100110011001100…1100(共52位,除了小数点左边的1),指数为-4(二进制移码为00000000010),符号位为0
=> 计算机存储为:0 00000000100 10011001100110011…11001
=> 因为尾数最多52位,所以实际存储的值为0.00011001100110011001100110011001100110011001100110011001
而十进制0.2
=> 二进制0.0011001100110011…(循环0011)
=>尾数为1.1001100110011001100…1100(共52位,除了小数点左边的1),指数为-3(二进制移码为00000000011),符号位为0
=> 存储为:0 00000000011 10011001100110011…11001
因为尾数最多52位,所以实际存储的值为0.00110011001100110011001100110011001100110011001100110011
那么两者相加得:
0.00011001100110011001100110011001100110011001100110011001
+ 0.00110011001100110011001100110011001100110011001100110011 (确认??)
= 0.01001100110011001100110011001100110011001100110011001100
转换成10进制之后得到:0.30000000000000004