精讀ECMAScript規範:截斷

之所以要寫這個,是因爲之前遇到了一個老生常談的問題:如何在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比較瞭解,應該會記得:

img

因爲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:

  1. Let number be ? ToNumber(argument).
  2. If number is NaN, +0, -0, +∞, or -∞, return +0.
  3. Let int be the Number value that is the same sign as number and whose magnitude is floor(abs(number)).
  4. Let int32bit be int modulo 232.
  5. If int32bit ≥ 231, return int32bit - 232; otherwise return int32bit.

所以,規範裏就已經規定了NaN會被轉換成0,0取反兩次自然還是0。

不過,還是那個問題,爲什麼NaN會對應到0?我覺得簡單一句“規定”並沒有說服力。我覺得可能是這樣:因爲NaN不是一個整數概念,而是一個浮點數概念(來源於IEEE 754的浮點數定義)。它在−231到231 − 1的範圍內找不到任何一個整數能表達這個概念,就只能委屈一下,跟0放在一起,因爲它代表的是“不存在”。Infinity同理,因爲在這個範圍內找不到無窮大,就只能跟0放在一起,表達一個“不存在”的概念。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章