JavaScript has two zeros: −0 and +0. This post explains why that is and where it matters in practice.
JavaScript 中有两个“0”: -0 和 +0 。这篇文章解释了为什么,并且指出实际生产中会造成什么影响
1.The signed zero
1.“-0”
Numbers always need to be encoded to be stored digitally. Why do some encodings have two zeros? As an example, let’s look at encoding integers as 4-digit binary numbers, via the sign-and-magnitude method. There, one uses one bit for the sign (0 if positive, 1 if negative) and the remaining bits for the magnitude (absolute value). Therefore, −2 and +2 are encoded as follows.
Binary 1010 is decimal −2
Binary 0010 is decimal +2
Naturally, that means that there will be two zeros: 1000 (−0) and 0000 (+0).
为了储存数字,需要将其编码为二级制,但为什么会编码出两个“0”呢,举个例子,将整数编码为4位二进制,由于整数有正有负,通过符号数值表示法,用第一位来表示符号(0 表示正数,1 表示负数),剩下的位表示数值(数的绝对值)。
所以,-2 和 +2 被编码为下面的形式:
二进制 1010 代表 -2
二进制 0010 代表 +2
很自然的,对于“2”也将出现两个:1000(-0) 和 0000(+0)
In JavaScript, all numbers are floating point numbers, encoded in double precision according to the IEEE 754 standard for floating point arithmetic. That standard handles the sign in a manner similar to sign-and-magnitude encoding for integers and therefore also has a signed zero. Whenever you represent a number digitally, it can become so small that it is indistinguishable from 0, because the encoding is not precise enough to represent the difference. Then a signed zero allows you to record “from which direction” you approached zero, what sign the number had before it was considered zero. Wikipedia nicely sums up the pros and cons of signed zeros:
在JavaScript中,所有的数字都被存储为浮点型数字,根据 IEEE 754 标准的浮点数算法编码为双精度浮点数。该标准类似于用符号数值表示法来编码整数,所以也会出现“-0”。当你要表示一个数字时,他可以表示一个小到与“0”区别不出来的数,因为编码方式无法足够精确的表示这种区别。用“-0”便可以记录一个数字在被认为是“0”之前,是“从坐标轴的那个方向”趋近真正的“0”的。关于“-0”的优劣,*做了很好的总结。
此段*引用的英文没有对应的中文版,所以自己做了翻译。
JavaScript goes to some lengths to hide the fact that there are two zeros.
JavaScript 做了很多工作来隐藏有两个“0”的事实。
2.Hiding the zero’s sign
2.隐藏“0”的符号
In JavaScript, you’ll usually write 0, which always means +0. But it also displays −0 simply as 0. The following is what you see when you use any browser command line or the Node.js REPL:
通常认为,JavaScript 中显示的“0”,表示的都是“+0”。其实“-0”也直接显示为“0”,下面的例子显示了浏览器命令行和Node.js中的执行情况:
- > -0
- 0
The reason is that the standard toString() method converts both zeros to the same "0".
原因是按照规则两个“0”都通过调用 toString() 方法转换成了相同的结果“0”:
- > (-0).toString()
- '0'
- > (+0).toString()
- '0'
The illusion of a single zero is also perpetrated by the equals operators. Even strict equality considers both zeros the same, making it very hard to tell them apart (in the rare case that you want to).
等于“==”操作符同样这样对待“-0”,甚至全等符号“===”也判断为他们相等,这使他们很难被区别(但在某些情况下需要区分)。
- > +0 === -0
- true
The same holds for the less-than and greater-than operators – they consider both zeros equal.
大于“>”小于“<”符号同样判断两个“0”相等。
- > -0 < +0
- false
- > +0 < -0
- false
3.Where the zero’s sign matters
3.“0”的符号都影响了哪些地方
The sign of the 0 rarely influences results of computations. And +0 is the most common 0. Only a few operations produce −0, most of them just pass an existing −0 through. This section shows a few examples where the sign matters. For each example, think about whether it could be used to tell −0 and +0 apart, a task that we will tackle in Sect. 4. In order to make the sign of a zero visible, we use the following function.
“-0”在一些很罕见的地方影响计算结果。通常“+0”就是通常的“0”。只有少数运算产生“-0”的结果,大多数都直接忽略“-0”的存在。这一段将展示“-0”在哪些情况下产生影响。想一想下面每一个例子要区别“-0”和“+0”的原因,在展示过程中,为了能清晰的看到“-0”,我们将使用下面这个函数。
- function signed(x) {
- if (x === 0) {
- // isNegativeZero() will be shown later 【isNegativeZero() 将在后文中给出实现】
- return isNegativeZero(x) ? "-0" : "+0";
- } else {
- // Otherwise, fall back to the default 【其它情况下,使用默认方法】
- // We don’t use x.toString() so that x can be null or undefined 【null 或 undefined 不能使用 x.toString() 的写法】
- return Number.prototype.toString.call(x);
- }
- }
3.1.Adding zeros
3.1.加法
Quoting Sect. 11.6.3 of the ECMAScript 5.1 specification, “Applying the Additive Operators to Numbers”:
For example:
引用 ECMAScript 5.1 规范第 11.6.3 节 “加法的变形” 的说明
如下:
- > signed(-0 + -0)
- '-0'
- > signed(-0 + +0)
- '+0'
This doesn’t give you a way to distinguish the two zeros, because what comes out is as difficult to distinguish as what goes in.
这并不能告诉你怎样区别两个“0”,因为运算的输入和输出一样难区别。
3.2.Multiplying by zero
3.2.乘法
When multiplying with zero with a finite number, the usual sign rules apply:
当两个非无穷数与“0”相乘时,就可以用通常的乘法规则了。
- > signed(+0 * -5)
- '-0'
- > signed(-0 * -5)
- '+0'
Multiplying an infinity with a zero results in NaN:
无穷数与“0”相乘,结果为非数字(NaN)
- > -Infinity * +0
- NaN
3.3.Dividing by zero
3.3.除法
You can divide any non-zero number (including infinities) by zero. The result is an infinity whose sign is subject to the usual rules.
用任何非零数(包括无穷)来除以“0”。结果符合通常的符号规则。
- > 5 / +0
- Infinity
- > 5 / -0
- -Infinity
- > -Infinity / +0
- -Infinity
Note that -Infinity and +Infinity can be distinguished via ===.
注意,正无穷和负无穷可以用“===”进行区别。
- > -Infinity === Infinity
- false
Dividing a zero by a zero results in NaN:“0”除以“0”为非数字(NaN)。
- > 0 / 0
- NaN
- > +0 / -0
- NaN
3.4.Math.pow()
3.4.乘方运算
The following is a table of the results of Math.pow() if the first argument is zero:
下表列出了以“0”为底数的乘法运算结果
- pow(+0, y<0) → +∞
- pow(−0, odd y<0) → −∞ //【奇数次幂】
- pow(−0, even y<0) → +∞ //【偶数次幂】
Interaction:
- > Math.pow(+0, -1)
- Infinity
- > Math.pow(-0, -1)
- -Infinity
3.5.Math.atan2()
3.5.极坐标弧度
The following is a table of the results that are returned if one of the arguments is zero.
下表列出了目标点横纵坐标为零时的返回值
- atan2(+0, +0) → +0
- atan2(+0, −0) → +π
- atan2(−0, +0) → −0
- atan2(−0, −0) → −π
- atan2(+0, x<0) → +π
- atan2(−0, x<0) → −π
Hence, there are several ways to determine the sign of a zero. For example:
因此,我们发现了区分两个零的方法,如:
- > Math.atan2(-0, -1)
- -3.141592653589793
- > Math.atan2(+0, -1)
- 3.141592653589793
atan2 is one of the few operations that produces −0 for non-zero arguments:
atan2 是少数几个能用非零参数产生“-0”的运算之一
- atan2(y>0, +∞) → +0
- atan2(y<0, +∞) → −0
Therefore:因此
- > signed(Math.atan2(-1, Infinity))
- '-0'
3.6.Math.round()
3.6.四舍五入
Math.round() is another operation that returns −0 for arguments other than −0 and +0:
Math.round()是另一个不用“-0”和“0”能产生“-0”的运算。
- > signed(Math.round(-0.1))
- '-0'
Here we have the effect that we talked about at the beginning: The sign of the zero records the sign of the value before rounding, “from which side” we approached 0.
现在我们可以体会前文中【用“-0”便可以记录一个数字在被认为是“0”之前,是“从坐标轴的那个方向”趋近真正的“0”的。】的含义了。
4.Telling the two zeros apart
4.区分两个“0”
The canonical solution for determining the sign of a zero is to divide one by it and then check whether the result is -Infinity or +Infinity:
一个典型的辨别“0”的符号的方法,就是检查用“1”除以它的运算结果是正无穷还是负无穷:
- function isNegativeZero(x) {
- return x === 0 && (1/x < 0);
- }
The above sections showed several other options. One original solution comes from Allen Wirfs-Brock. Here is a slightly modified version of it:
前文也展示了另外一些选择。Allen Wirfs-Brock 还提供了一种基于对象原型的方法,这里有一个稍作修改的版本。
- function isNegativeZero(x) {
- if (x !== 0) return false;
- var obj = {};
- Object.defineProperty(obj, 'z', { value: -0, configurable: false });
- try {
- // Is x different from z’s previous value? Then throw exception.【如果 x 与前面定义的 z 取值不同,则会抛出异常。】
- Object.defineProperty(obj, 'z', { value: x });
- } catch (e) {
- return false
- };
- return true;
- }
Explanation: In general, you cannot redefine a non-configurable property – an exception will be thrown. For example:
说明:通常,你不能重定义一个“不可配置”(non-configurable)的属性,如果这么做会抛出下面这个异常:
- TypeError: Cannot redefine property: z
However, JavaScript will ignore your attempt if you use the same value as the existing one. In this case, whether a value is the same is not determined via ===, but via an internal operation that distinguishes −0 and +0. You can read up on the details in Wirfs-Brock’s blog post (freezing an object makes all properties non-configurable).
JavaScript 试图会忽略你对用相同的值进行的修改。在这个例子中,值相同并不是通过全等运算符“===”来判断的,是通过一种内在的机制来区分“-0”和“+0”的。详细内容可以去读 Wirfs-Brock 的博文“设置所有属性为不可配置实现对象锁定(freezing an object makes all properties non-configurable)”
5.Conclusion
5.结论
We have seen that there are two zeros, because of how the sign is encoded for JavaScript’s numbers. However, −0 is normally hidden and it’s best to pretend that there is only one zero. Especially, because the difference between the zeros has little bearing on computations. Even strict equality === can’t tell them apart. Should you, against all expectations or just for fun, need to determine the sign of a zero, there are several ways to do so. Note that the slightly quirky existence of two zeros is not JavaScript’s fault, it simply follows the IEEE 754 standard for floating point numbers.
我们已经看到两个零的符号在 JavaScript中是怎样编码的。虽然通常情况下“-0”被隐藏的很好,伪装成就像只有一个“0”存在的样子。特别是一些运算掩盖了这点小小的不同。使全等“===”操作也无法区别他们。如果你想要区分两个“0”,不管是有意为之,还是只为了好玩儿,这里已经提供了一些方法。要注意,有两个“0”这一点儿古怪之处,并不是 JavaScript 的bug,而是遵从了 IEEE 754 规范的浮点数规则。
6.Related reading
6.扩展阅读
This post is part of a series on JavaScript numbers that comprises the following posts:
- Integers and shift operators in JavaScript
- Displaying numbers in JavaScript
Furthermore, the blog post “Stricter equality in JavaScript” examines that === cannot detect either the value NaN or the sign of a zero.
本文是几篇讨论 JavaScript 中数字的系列文章中的一篇,系列的其他文章在下面:
另外,JavaScript中的严格比较研究了“===”不能用于检查 NaN 或 “-0” 的各种情况。