[譯]JavaScript中的兩個0


原文:http://www.2ality.com/2012/03/signedzero.html


譯者注:文章開始之前,先看道題:

Puzzle: A === B; 1/A < 1/B; A = ?

你知道A等於什么嗎?

JavaScript中有兩個0:-0和+0.本文解釋了為什么會這樣,以及它會產生哪些影響.

1. 帶符號的0

數字需要被編碼才能進行數字化存儲.舉個例子,假如我們要將一個整數編碼為4位的二進制數,使用原碼(sign-and-magnitude)方法,則最高位是符號位(0代表正,1代表負),剩下的三位表示大小(具體的值).因此,−2和+2會編碼成為下面這樣:

二進制的1010表示十進制的−2
二進制的0010表示十進制的+2

這就意味着將會有兩個零:1000(-0)和0000(0).

在JavaScript中,所有的數字都是浮點數,都是根據IEEE 754標准中的浮點數算法以雙精度格式被編碼.這個標准中正負號的處理方式類似於原碼(sign-and-magnitude)方法中整數的編碼方式,所以也有正負零.

JavaScript做了不少工作來故意隱藏存在有兩個零的事實.

2. 隱藏0的符號

在JavaScript中,如果你寫個0,則意味着就是+0.但是即使你寫−0,引擎也會顯示為0.無論你在任何的瀏覽器命令行或Node.js REPL中執行,都會這樣顯示:

> -0
0

譯者注:SpiderMonkey shell(js)以及v8 shell(d8)都會顯示出-0.

原因是標准的toString()方法會將兩種零都轉換成相同的"0".

> (-0).toString()
'0'
> (+0).toString()
'0'

嚴格相等運算符也讓我們有了"只存在一個零"的錯覺.連嚴格相等都無法區分開,這給我們區分它們帶來了很大困難(在某些少有的情況下,你的確需要區分它們).

> +0 === -0
true

小於號和大於號運算符也類似,它們也認為這兩個零是相等的.

> -0 < +0
false > +0 < -0 false

3. 零的正負號對計算結果的影響

0的正負號很少會影響計算結果.並且我們通常能見到的0都是+0.只有極少數操作會產生−0的結果,而且其中大多數操作都必須刻意傳入-0才有可能得到-0的結果.本節會演示幾個0的正負號影響計算結果的例子.對於每個例子,想想能不能利用它們來區分開−0和+0,最終的解決方案將會在第四節中給出.為了能讓一個零的正負號顯示出來,我們使用下面的這個函數.

function signed(x) {
if (x === 0) {
// isNegativeZero()函數將會在下面給出 return isNegativeZero(x) ? "-0" : "+0";
}
else {
// 否則,使用數字原生的toSting方法 return Number.prototype.toString.call(x);
}
}

3.1. 兩個零相加

引用自ES5 11.6.3 "加法運算符作用於數字的情況":

兩個負零的和是−0.兩個正零或者一個正零和一個負零的和是+0.

例如:

> signed(-0 + -0)
'-0'
> signed(-0 + +0)
'+0'

-0和-0的和仍然是-0,+0和-0的和仍然是+0,我們仍無法利用它們的和來區分正負0.

3.2. 乘零

用零乘以一個有窮數,結果的正負號符合一般規則:

> signed(+0 * -5)
'-0'
> signed(-0 * -5)
'+0'

用零乘以一個無窮數的結果是NaN:

> -Infinity * +0
NaN

3.3. 除以零

你可以用零除任何非零的數字(包括無窮大).返回的結果是無窮大,正負號也符合一般規則.

> 5 / +0
Infinity
> 5 / -0
-Infinity
> -Infinity / +0
-Infinity

需要注意的是-Infinity和+Infinity可以使用===來區分.

> -Infinity === Infinity
false

用零除零結果是NaN:

> 0 / 0
NaN
> +0 / -0
NaN

3.4. Math.pow()

下面的表格列出了,當第一個參數是零時,函數Math.pow()的返回結果:

pow(+0, y<0) → +∞
pow(−0, odd y<0) → −∞
pow(−0, even y<0) → +∞

試驗一下:

> Math.pow(+0, -1)
Infinity

> Math.pow(-0, -1)
-Infinity

3.5. Math.atan2()

下面的表格列出了,當有參數為零時,函數Math.atan2()的返回結果:

atan2(+0, +0) → +0
atan2(+0, −0) → +π
atan2(−0, +0) → −0
atan2(−0, −0) → −π

atan2(+0, x<0) → +π
atan2(−0, x<0) → −π

根據上表,可以得出好幾種辦法來判斷出一個零的正負號.例如:

> Math.atan2(-0, -1)
-3.141592653589793
> Math.atan2(+0, -1)
3.141592653589793

atan2函數是少有的幾個參數不為零卻能產生-0結果的操作之一:

atan2(y>0, +∞) → +0
atan2(y<0, +∞) → −0

因此:

> signed(Math.atan2(-1, Infinity))
'-0'

3.6. Math.round()

Math.round()是另外一個參數不為零卻產生-0結果的操作:

> signed(Math.round(-0.1))
'-0'

4. 區分這兩個零

判斷一個零是正還是負的標准解法是用它除1,然后看計算的結果是-Infinity還是+Infinity:

function isNegativeZero(x) {
return x === 0 && (1/x < 0);
}

除了上面講的幾種解法.還有一個解法來自Allen Wirfs-Brock(譯者注:TC39編輯,ES標准就是他寫出來的.):

function isNegativeZero(x) {
if (x !== 0) return false;
var obj = {};
Object.defineProperty(obj,
'z', { value: -0, configurable: false });
try {
// 如果x的值和z屬性的當前值不相等的話,就會拋出異常. Object.defineProperty(obj, 'z', { value: x });
}
catch (e) {
return false }; return true;
}

解釋:通常情況下,你不能重新定義一個不可配置的對象屬性,否則會拋出異常:

    TypeError: Cannot redefine property: z

可是,如果你重新定義屬性時指定的屬性特性的值與該特性當前的值相等,則JavaScript會忽略掉這個重定義操作,不會拋出異常.其中在判斷兩個值是否相等時使用的運算不是===,而是一個稱之為SameValue的內部算法,該算法可以區分開-0和+0.你可以從Wirfs-Brock的原文中了解更多細節(凍結一個對象會讓該對象的所有屬性變的不可配置).

5. 總結

通過本文我們知道了,由於JavaScript中數字正負號的編碼方式,導致了JavaScript中存在有兩個不同符號的零.不過,−0的負號通常會被引擎有意隱藏起來,我們也最好假裝只有一個零存在.這主要是因為,正負零之間的區別幾乎不會影響到我們正常的計算操作.即便是嚴格相等運算符===也不能區分開它們.如果你的確需要區分一個零到底是正還是負,文中也給出了幾種解決方法.需要注意的是,這種存在有兩個零的怪異表現並不是JavaScript的錯誤,這只是遵循了IEEE 754中關於浮點數的標准.

6. 相關鏈接


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM