之所以要写这个,是因为之前遇到了一个老生常谈的问题:如何在JS中实现截断,也就是向0取整,也就是保留数字的整数部分?
JS使用的是基于IEEE 754的浮点数,这个大家都知道。IEEE 754所带来的浮点数精度问题(比如著名的0.1 + 0.2 != 0.3
,==
还是===
都是一样的,因为都是number),以及大整数的误差问题(比如著名的2**53 + 1 == 2**53
),我们也都很清楚。关于这些问题,ES6以后提供了类似于Number.EPSILON
、BigInt等一系列特性去解决这些问题,就不在这里赘述了。
首先需要说明的是,截断并不代表向下取整。比如对于-0.2,如果是截断,结果应该是0,而向下取整则是-1。事实上,截断的实现相当于这样:
function trunc(x) {
return x < 0 ? Math.ceil(x) : Math.floor(x);
}
因为这个场景还是比较常用的,所以ES6提供了原生的Math.trunc来实现截断;当然了,这个有兼容性的问题,并不是所有浏览器都支持ES6(没错我说的就是IE)。
原生的实现有一个非常有趣的地方,如果参数本身是一个整数,那返回的结果也是整数。啥意思呢?意思是:
Math.trunc(20) // 20
Math.trunc(20.0) // 20
Math.trunc(20n) // Uncaught TypeError: Cannot convert a BigInt value to a number
只要这个数字事实上是一个number类型的整数(20.0事实上也是一个number类型的整数),返回值就是一个整数,这也是Math里很多函数的共同特点。不过,如果传入的参数是一个BigInt类型的整数,那么就会报错。虽然我们都知道,20n他就是一个整数,而且也在number的范围内,但类型不同就是不同,没办法执行。
事实上,规范里明确定义了“整数”的概念:
When the term integer is used in this specification, it refers to a Number value whose mathematical value) is in the set of integers, unless otherwise stated: when the term mathematical integer is used in this specification, it refers to a mathematical value which is in the set of integers.
也就是说,“整数”指的是number类型中的整数,而“数学整数”才是数学意义上的整数集;BigInt就属于“数学整数”,和普通的数字不是一个类型的,无法互操作。
我们看看规范里怎么定义Math.trunc
的行为的:
Returns the integral part of the number x, removing any fractional digits. If x is already an integer, the result is x.
- If x is NaN, the result is NaN.
- If x is -0, the result is -0.
- If x is +0, the result is +0.
- If x is +∞, the result is +∞.
- If x is -∞, the result is -∞.
- If x is greater than 0 but less than 1, the result is +0.
- If x is less than 0 but greater than -1, the result is -0.
规范里也强调了,该方法只适用于number类型。
事实上,截断的实现方式有很多种,其中流传最广的几种之一,就是位运算。比如:
const n = -1.23;
~~n; // -1
n | 0; // -1
但是我们刚才看到了规范里对trunc的定义,位运算只能实现其中的一部分。比如这里以~~
为例:
~~NaN // 0
~~+0 // 0
~~-0 // 0
~~+Infinity // 0
~~-Infinity // 0
~~-0.5 // 0
~~(2**31) // -2147483648(-2**31)
我们可以看到,因为位运算的特性,特殊值的输出全是0,无法分辨。此外,因为位运算只支持32位,所以一旦超过32位有符号整数的范围,就会出现错误的结果(超过32位有符号整数的范围后,截取低32位)。
但是,有一个非常蹊跷的地方:为什么~~NaN
会是0?如果大家对IEEE 754比较了解,应该会记得:
因为JS的位运算是截取低32位,所以~~Infinity
为0是可以理解的,因为Infinity的低32位都为0。但是NaN的低32位并不一定全为0,并且NaN不是一个数,而是一个范围;为什么还是0呢?
规范上规定了这个行为的过程。还是以取反操作为例:
The abstract operation Number::bitwiseNOT takes argument x (a Number). It performs the following steps when called:
Let oldValue be ! ToInt32(x).Return the result of applying bitwise complement to oldValue. The result is a signed 32-bit integer.
相当于是先转换成32位的整数,然后再取反。而这个转换过程是这样的:
The abstract operation ToInt32 takes argument argument. It converts argument to one of 232 integer values in the range -231 through 231 - 1, inclusive. It performs the following steps when called:
- Let number be ? ToNumber(argument).
- If number is NaN, +0, -0, +∞, or -∞, return +0.
- Let int be the Number value that is the same sign as number and whose magnitude is floor(abs(number)).
- Let int32bit be int modulo 232.
- If int32bit ≥ 231, return int32bit - 232; otherwise return int32bit.
所以,规范里就已经规定了NaN会被转换成0,0取反两次自然还是0。
不过,还是那个问题,为什么NaN会对应到0?我觉得简单一句“规定”并没有说服力。我觉得可能是这样:因为NaN不是一个整数概念,而是一个浮点数概念(来源于IEEE 754的浮点数定义)。它在−231到231 − 1的范围内找不到任何一个整数能表达这个概念,就只能委屈一下,跟0放在一起,因为它代表的是“不存在”。Infinity同理,因为在这个范围内找不到无穷大,就只能跟0放在一起,表达一个“不存在”的概念。