JavaScript中的数字运算大概是最让人迷惑的了,我看过许多讲述JS怪异之处的资料都会举一个列子:
0.1 + 0.2; // 0.30000000000000004
除了跟风喊几句什么鬼之外,让我们一起来探索一下隐藏在背后的东西。在计算机的世界,有果必有因。
首先要明确一个概念,JavaScript中的所有数字都是浮点数,并且是符合IEEE754标准的双精度浮点数。一般我们看到的类似整数的东西其实都是浮点数,只不过是小数点后没有数字,不显示小数部分而已。
数字字面量
尽管语言内部表示只有浮点数,但数字字面量表示还是可以有整数和浮点数的。因此我们可以直接输入35,而不用35.0来表示这个数字。
JS中的数字还有几个特殊值:
- 表示错误的值:NaN和Infinity
- 用于某些数学计算的+0和-0
这几个特殊值很容易让人摸不清头脑,他们本身也有许多反直觉的地方,且让我细细道来。
NaN
它是Not A Number的缩写,表示值不是数字。比如尝试将'aa'解析为数字:
Number('aa'); // NaN
看起来好像很简单,但这里有不少陷阱,一不小心就会给你带来难以察觉的bug。首先虽然NaN表示的是不是数字,但如果我们用typeof检查它自身的类型:
typeof NaN; //Number
表示不是数字的东西自身却是个数字,这个逻辑让我不敢直视。
然后如果我们严格比较两个NaN:
NaN === NaN //false
好吧,明明是一样的东西,类型也同是Number,居然是不相等的!NaN也是JS中唯一一个不等于自身的值。所有要判断某个值是否是NaN不能直接比较,而要用原生的isNaN方法:
isNaN(NaN); //true
这还没有完,如果你将非数字字面量传入isNaN方法,你很可能得到的时true!
isNaN('aaa'); //true
可'aaa'明明不是NaN啊!这是因为'aaa'先被隐性转换成数字,然后再传入isNaN,也就是类似:
isNaN(Number('aaa'));
前面提过,Number('aaa')的结果是NaN,所以isNaN自然为true。要躲过这个陷阱,必须在判断时多加一个判断是否是数字的条件:
if (typeof value === 'number' && isNaN(value))
还有一个更简单的方法,利用前面提到的NaN是唯一一个不和自己相等的东西:
if (value !== value)
于是只要value是NaN,这里就一定为true,其他情况则一律为false。
Infinity
再来看看Infinity,这个值通常用来表示一个超出表示范围的值,当一个数字除0的时候也会返回Infinity。Infinity同样有+Infinity和-Infinity。比起NaN,Infinity要友善的多,你可以直接用===来判断,也可以用内置的isFinite()来做判断。
+0/- 0
最后再来看看正零和负零。这样反常识的表示方法在某些数学运算领域是很有用的,比如在表示趋于零的极限时,正零和负零可以帮助我们表示趋近的方向。但一般情况下,我们可粗略的认为只有一个0,不需要做特别的区分。
(-0).toString() //'0' (+0).toString() //'0'
数字的内部表示
本文的开头说过,JavaScipt中的所有数字都是64位的双精度浮点数。这里我们就来详细看看这种浮点数在内部是如何表示的,以及这种表示方法的一些问题和局限。
JS中的浮点数由三部分组成:
- 符号占1位
- 指数部分占11位
- 小数部分占52位
这三部分加起来就是64位了。
64位浮点数的内部表示
而计算机内部都是二进制实现的,所以数字计算公式为:
(–1)sign × %1.fraction × 2exponent
%表示二进制。
这样的表示方法有什么问题呢?这就是开篇提出的那个例子:
0.1 + 0.2; // 0.30000000000000004
更奇怪的是,按常识我们都知道加法的结合律,既(a+b)+c = a+(b+c)。但在JS中这个公里不成立:
(0.1 + 0.2) + 0.3; // 0.6000000000000001 0.1 + (0.2 + 0.3); // 0.6
让我们来详细探讨一下这个问题是怎么产生的。我们先来看看我们习惯使用的十进制。十进制的小数可以用分数的形式表示:m/10^e
可以看到,分母部分都是十的次方。十进制也有不能精确表示的分数,比如1/3就不能被精确表示。这是因为分母不含3,因此必然无法表示为10^e。
再来看二进制,同样的分数表示法,只不过分母是2的次方而已。因此我们可以得出结论,只要分母部分不是2的次方的,都无法精确表示为2^e:
0.5dec = 5/10 = 1/2 = 01bin
0.75dec = 75/100 = 3/4 = 0.11bin
0.1dec = 1/10 = 1/2X5
0.2dec = 2/10 = 1/5
现在你明白为什么 0.1 + 0.2是0.30000000000000004了吧。我们在console里看到的0.1并不是完整的内部表示,稍微做点处理就能看到内部表示了:
0.1 * Math.pow(10, 24) //1.0000000000000001e+23
那么在需要精确计算的时候有什么方法吗?有的,那就是使用整数。整数没有小数部分的舍入问题,可以准确地被表示。例如在金融方面,可以按最小单位的整数来表示钱。比如0.55元表示为55分,按55来进行计算。
另外也要注意,直接比较小数可能会带来不可知的结果,最好使用Machine_epsilon来进行比较:
var EPSILON = Math.pow(2, -53);
function epsEqu(x, y) {
return Math.abs(x - y) < EPSILON;
}
整数
前面说过,JS中并不存在真正的整数。整数都是用浮点数表示的,但是要注意有一处例外的地方,那就是位运算。JS中的位运算会先将数字转换为32位整数,运算完成后返回的结果也是32位整数。
另外,由于整数是由浮点数表示的,这里有一个安全整数的概念。JS中的安全整数指的是范围在(−253, 253)内的整数。我们说他们是安全的,是因为在这个范围内可以保证每个整数只有一个对应的浮点数表示形式。超过这个范围则会出现多个浮点表示形式。因此在JS中做整数运算时,最好保证运算的整数都在这个安全范围之内。一定要处理大整数的话必须依赖相应的类库,否则结果很可能不准确。
在写完这篇文章之后,JS之父Brendan Eich透露了在ES7中会支持64位大整数。JS渐渐摆脱了玩具语言的束缚,向更广阔的天地出发了。跟上了,程序员。
网友评论