JavaScript 中比较运算的详细总结

阅读 532
收藏 47
2017-02-17
原文链接:gcidea.info

前言

  在javascript开发中,比较操作是十分常见的。由于显式/隐式强制类型转换机制的存在,我们在使用比较运算时显得过于随意,也许表面上看并没有什么错误(比如在if()语句中判断两值相等时顺手就写成 == ),但是这可能会埋下很多不易发现的隐患。对于比较操作(相等关系和不等关系),在javascript中其实是有一套完善的机制的。本文依据ES5规范《ECMAScript Language Specification ECMA-262
5.1 Edition / June 2011》
的11.8节和11.9节:

对javascript中比较操作相关内容进行系统总结。


相等比较

严格相等

  严格相等指“===”,它不允许比较双方进行强制类型转换。因此,问题考虑变得简单,对于x === y,javascript引擎在进行判断时所遵循的算法如下:

  1. 如果x和y的数据类型不同,返回false。
  2. 如果x是undefined,返回true。
  3. 如果x是null,返回true。
  4. 如果x是number类型:
     4.1 如果x是NaN,返回false。
     4.2 如果y是NaN,返回false。
     4.3 如果x和y的值相等,返回true。
     4.4 如果x是+0,y是-0,返回true。
     4.5 如果x是-0,y是+0,返回true。
     4.6 否则,返回false。
  5. 如果x是string类型,如果x和y是长度相等且对应位置上字符相同的序列,返回true;否则返回false。
  6. 如果x是Boolean类型,如果x和y均为true或者x和y均为false,返回true;否则,返回false。
  7. 如果x是对象(普通对象,函数,数组等),那么如果x和y指向同一个对象(是内存中同一个对象的引用),返回true;否则,返回false。

宽松相等

  宽松相等指“==”,它会对比较双方进行隐式强制类型转换。下面先根据ES5规范进行系统说明:

  1. 如果x和y的数据类型相同:
     1.1 如果x的类型是undefined,返回true。
     1.2 如果x的类型是null,返回true。
     1.3 如果x的类型是number:
      1.3.1 如果x是NaN,返回false。
      1.3.2 如果y是NaN,返回false。
      1.3.3 如果x和y的值相同,返回true。
      1.3.4 如果x是+0,y是-0,返回true。
      1.3.5 如果x是-0,y是+0,返回true。
      1.3.6 否则,返回false。
     1.4 如果x是string类型,如果x和y是长度相等且对应位置上字符相同的序列,返回true;否则返回false。
     1.5 如果x是Boolean类型,如果x和y均为true或者x和y均为false,返回true;否则,返回false。
     1.6 如果x是对象(普通对象,函数,数组等),那么如果x和y指向同一个对象(是内存中同一个对象的引用),返回true;否则,返回false。
  2. 如果x是null,y是undefined,返回true。
  3. 如果x是undefined,y是null,返回true。
  4. 如果x是number类型,y是string类型,则对y进行类型转换,转换为number类型,返回 x == ToNumber(y) 的结果(参见上述1.3)。
  5. 如果x是string类型,y是number类型,则对x进行类型转换,转换为number类型,返回 ToNumber(x) == y 的结果(参见上述1.3)。
  6. 如果x是Boolean类型,则对x进行类型转换,转换为number类型,返回 ToNumber(x) == y 的结果(此时y的类型还是不确定的,应将其转换为number类型后进行比较)。
  7. 如果y是Boolean类型,则对y进行类型转换,转换为number类型,返回 x == ToNumber(y) 的结果(此时x的类型还是不确定的,应将其转换为number类型后进行比较)。
  8. 如果x是string类型或者number类型,y是一个对象(普通对象,函数,数组等),则对y进行类型转换—使用内置的[[ToPrimitive]]方法转换(该方法简单来说,就是先调用该对象上的valueOf()方法,如果有该方法且返回基本类型值,就使用该值进行强制类型转换;如果不存在,就调用该对象上的toString()方法,如果有该方法,就使用其返回值来进行强制类型转换;如果这两个方法都不存在,就产生TypeError错误。),返回 x == ToPrimitive(y) 的结果。
  9. 如果x是一个对象(普通对象,函数,数组等),y是string类型或者number类型,则对x进行类型转换—使用内置的[[ToPrimitive]],返回 ToPrimitive(x) == y 的结果。
  10. 否则(非上述所有情况),返回false。

注:针对以上10条有几点注意事项:
1.对a,b强制按字符串string类型进行比较,可采用如下方法:
"" + a == "" + b

2.对a,b强制按数字number类型进行比较,可采用如下方法:
+a == +b

3.对a,b强制按布尔值boolean类型进行比较,可采用如下方法:
!!a == !!b

4.相等比较操作有以下恒等性:

  • A != B 等价于 !(A == B)
  • A == B 等价于 B == A (除非A B有顺序上的互相计算关系)

5.相等操作并不是总具有传递性。
new String(“a”) == “a” 和 “a” == new String(“a”) 结果都返回true;
new String(“a”) == new String(“a”) 却返回false。
因为new String(“a”)是一个对象,按照上述规则会被转换为”a”,因此相等;而new String(“a”) == new String(“a”),==左右两边是两个不同的对象,在内存中位于不同地址,因此结果返回false。


由于强制类型转换的存在,宽松相等的情况变得复杂,比较容易出现问题的有以下几种:

更改内置原生原型后的相等比较

Number.prototype.valueOf = function(){
return 3;
}

new Number(2) == 3; // true

这只是为了说明这种情况的存在,应该不会有人这么去改原型上的方法。

假值的相等比较

这部分应该是比较复杂的:

"0" == null;           
//false
"0" == undefined;
//false

"0" == false;
//true
"0" == NaN;
//false
"0" == 0;
//true
"0" == "";
//false

false == null;
//false
false == undefined;
//false
false == NaN;
//false
false == 0;
//true
false == "";
//true
false == [];
//true
false == {};
//false

"" == null;
//false
"" == undefined;
//false
"" == NaN;
//false
"" == 0;
//true
"" == [];
//true
"" == {};
//false

0 == null;
//false
0 == undefined;
//false
0 == NaN;
//false
0 == [];
//true
0 == {};
//false

解释如下:

"0" == null;          
//false:null转换为"null"
"0" == undefined;
//false:undefined转换为"undefined"
"0" == false;
//true:false转换为0;"0"转换为0
"0" == NaN;
//false:NaN转换为"NaN"
"0" == 0;
//true:"0"转换为0
"0" == "";
//false:都是字符串,值不同

false == null;
//false:false转换为0;null转换为"null",进而转换为数字,得到NaN
false == undefined;
//false:false转换为0;undefined转换为"undefined",进而转换为数字,得到NaN
false == NaN;
//false:false转换为0;与NaN不同
false == 0;
//true:false转换为0
false == "";
//true:false转换为0;""转换为0
false == [];
//true:false转换为0;[]转换为0
false == {};
//false:false转换为0;{}转换为NaN

"" == null;
//false:""转换为0;null转换为"null",进而转换为数字,得到NaN
"" == undefined;
//false:""转换为0;undefined转换为"undefined",进而转换为数字,得到NaN
"" == NaN;
//false:""转换为0;与NaN不同
"" == 0;
//true:""转换为0
"" == [];
//true:""转换为0;[]转换为0
"" == {};
//false:""转换为0;{}转换为NaN

0 == null;
//false:null转换为"null",进而转换为数字,得到NaN
0 == undefined;
//false:undefined转换为"undefined",进而转换为数字,得到NaN
0 == NaN;
//false:0与NaN不同
0 == [];
//true:[]转换为0
0 == {};
//false:{}转换为NaN

一些极端情况

a. [] == ![]; //true
  []被转换为0,![]被转换为false,进而被转换为0。
b. 2 == [2]; //true
  [2]调用数组的valueOf()方法,返回”2”,进而被转换为2。
c. “” == [null]; //true
  [null]调用数组的valueOf()方法,进而调用toString()方法,返回””。   


一些选用的原则

  1. 如果==两边有true或者false(指本身,不是经过类型转换以后的),绝对不使用==。
  2. 如果==两边有[],””,0,尽量不使用==。
  3. ==和===选取哪一个取决于是否允许比较双方进行强制类型转换。
  4. 不应该一味地使用===来避免考虑这些可能的问题,因为有时候隐式类型转换可以让代码更加简洁,只要用的对。‘
  5. typeof x == "function"typeof x != "undefined"这样的用法是完全正确且安全的,开发中也常用。

经典的相等比较关系图

GitHub上有一个经典的比较关系图,dorey.github.io/JavaScript-…,以表格的形式系统总结了宽松相等==,严格相等===,if()条件语句中使用不同数据类型对应的结果,很有意义:


不等比较

!= 和 !==

  只要搞清楚上面详细描述的==和===,对应取反即可。

> < >= <=

这些比较的基础是 x < y:

x < y会返回true或者false或者undefined。如果返回undefined,说明x,y两者至少有一个是NaN。比较算法中需要使用一个布尔值的标记LeftFirst作为参数。这个参数的作用是控制可能具有副作用的操作作用于x和y的顺序。这个标志是有必要的,因为在ECMAScript指定了从左到右的运算顺序,LeftFirst的默认值是true,表示x的表达式是在y的表达式左边的。如果LeftFirst值为false,情况相反,表明关于y的操作必须先于x进行。据此,比较操作规则如下:

  1. 如果LeftFirst值为true:
     1.1 ToPrimitive(x, hint Number)的结果记为px
     1.2 ToPrimitive(y, hint Number)的结果记为py
  2. 否则,运算顺序改为从右向左:
     2.1 ToPrimitive(y, hint Number)的结果记为py
     2.2 ToPrimitive(x, hint Number)的结果记为px
  3. 经过如上转换,如果px和py至少有一个的类型不为string:
     3.1 ToNumber(px)的结果记为nx
     3.2 ToNumber(py)的结果记为ny
     3.3 如果nx是NaN,返回undefined
     3.4 如果ny是NaN,返回undefined
     3.5 如果nx和ny的值相同,返回false
     3.6 如果nx是+0,ny是-0,返回false
     3.7 如果nx是-0,ny是+0,返回false
     3.8 如果nx是+Infinity,返回false
     3.9 如果ny是+Infinity,返回true
     3.10 如果ny是-Infinity,返回false
     3.11 如果nx是-Infinity,返回true
     3.12 如果nx的值小于ny,返回true;否则,返回false
  4. 如果px和py都是string:
     4.1 如果py是px的前缀,返回false
     4.2 如果px是py的前缀,返回true
     4.3 设置变量k,k表示px和py对应位上出现不同值时,位置的索引。
     4.4 记px的位置k上的字母对应的字符编码值为m
     4.5 记py的位置k上的字母对应的字符编码值为n
     4.6 如果m < n,返回true;否则返回false

因此,> < >= <=就有如下的规则:  

RelationalExpression > ShiftExpression

  1. 记lref为RelationalExpression的运算结果值。
  2. 记lval为lref类型转换后获取的value值。
  3. 记rref为ShiftExpression的运算结果值。
  4. 记rval为rref类型转换后获取的value值。
  5. 设LeftFirst为false,将rval < lval按照上述规则进行运算,结果为r。
  6. 如果r是undefined返回false,否则返回r。

RelationalExpression < ShiftExpression

  1. 记lref为RelationalExpression的运算结果值。
  2. 记lval为lref类型转换后获取的value值。
  3. 记rref为ShiftExpression的运算结果值。
  4. 记rval为rref类型转换后获取的value值。
  5. 将lval < rval按照上述规则进行运算,结果为r。
  6. 如果r是undefined返回false,否则返回r。

RelationalExpression >= ShiftExpression

  1. 记lref为RelationalExpression的运算结果值。
  2. 记lval为lref类型转换后获取的value值。
  3. 记rref为ShiftExpression的运算结果值。
  4. 记rval为rref类型转换后获取的value值。
  5. 设LeftFirst为false,将rval < lval按照上述规则进行运算,结果为r。
  6. 如果r是true或者undefined返回false,否则返回true。

RelationalExpression <= ShiftExpression

  1. 记lref为RelationalExpression的运算结果值。
  2. 记lval为lref类型转换后获取的value值。
  3. 记rref为ShiftExpression的运算结果值。
  4. 记rval为rref类型转换后获取的value值。
  5. 将lval < rval按照上述规则进行运算,结果为r。
  6. 如果r是true或者undefined返回false,否则返回true。
评论