导航栏: 首页 评论列表

三种不同的值比较操作 关于正负零

默认分类 2021/06/21 04:09

ES 2015/ ES 6 新增了一个方法用于 javascript 相等性判断 – Object.is()。它与之前的相等比较运算符有什么不同呢?

[](#三种不同的值比较操作 "三种不同的值比较操作")三种不同的值比较操作

通过查看规范,

比较 x === y,x 和 y 为值,需要产出 true 或 false。比较过程如下:

  - 如果 Type(x) 与 Type(y) 的结果不一致,返回 false。否则 -
  - 如果 Type(x) 结果为 Undefined,返回 true。
  - 如果 Type(x) 结果为 Null,返回 true。
  - 如果 Type(x) 结果为 Number,则
    - 如果 x 为 NaN,返回 false。
    - 如果 y 为 NaN,返回 false。
    - 如果 x 与 y 为同一个数字,返回 true。
    - 如果 x 为 +0,y 为 -0,返回 true。
    - 如果 x 为 -0,y 为 +0,返回 true。
    - 返回 false。
  - 如果 Type(x) 结果为 String,如果 x 与 y 为完全相同的字符序列(相同的长度和相同的字符对应相同的位置),返回 true,否则,返回 false。
  - 如果 Type(x) 结果为 Boolean,如果 x 与 y 都为 true 或 false,则返回 true,否则,返回 false。
  - 如果 x 和 y 引用到同一个 Object 对象,返回 true,否则,返回 false。

全运算符与 SameValue 算法在对待有符号的 NaN 上表现不同。

全运算符虽然没有作类型转换,但也是针对 NaN+0-0 作了特殊处理。

Object.is 是ES2015 新特性,与 === 不一样的是:它可以正确分辨 正负零 及 NaN。

-==============================================================

原文: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

原因是标准的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&lt;0) → +∞ 
pow(−0, odd y&lt;0) → −∞ 
pow(−0, even y&lt;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&lt;0) → +π  
atan2(−0, x&lt;0) → −π

根据上表,可以得出好几种办法来判断出一个零的正负号.例如:

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

atan2函数是少有的几个参数不为零却能产生-0结果的操作之一:

atan2(y&gt;0, +∞) → +0 
atan2(y&lt;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的原文中了解更多细节(冻结一个对象会让该对象的所有属性变的不可配置).


>> 留言评论