【JavaScript】深入分析JavaScript的关系运算和if语句

JavaScript的关系运算,没有我原想的那么简单。等终于理清它的运算逻辑之后,我的头大了至少一圈。而if语句的真假判定逻辑本身不难,但要把它和关系运算联系起来,相信你会和我一样,到达崩溃边缘。不信,请跟我来。

JavaScript的关系运算包括比较运算和等值运算两种。其中,比较运算包括:<、<=、>、>=,等值运算包括:==、!=、===、!==。因为===和!==运算比较简单,不容易和其它运算发生混淆,所以这里就不再讨论了。

先看比较运算符。由于>、>=和<、<=的运算相似,这里只作<和<=的运算解析。至于>和>=运算,参考<和<=运算即可。

小于运算符 ( < )

产生式 RelationalExpression : RelationalExpression < ShiftExpression 按照下面的过程执行 :

  1. 令 lref 为解释执行 RelationalExpression 的结果 .
  2. 令 lval 为 GetValue(lref).
  3. 令 rref 为解释执行 ShiftExpression 的结果 .
  4. 令 rval 为 GetValue(rref).
  5. 令 r 为抽象关系比较算法 lval < rval( 参见 11.8.5) 的结果
  6. 如果 r 为 undefined,返回 false. 否则 , 返回 r.

摘自:http://yanhaijing.com/es5/#193

小于等于运算符 ( <= )

产生式 RelationalExpression : RelationalExpression <= ShiftExpression 按照下面的过程执行 :

  1. 令 lref 为解释执行 RelationalExpression 的结果 .
  2. 令 lval 为 GetValue(lref).
  3. 令 rref 为解释执行 ShiftExpression 的结果 .
  4. 令 rval 为 GetValue(rref).
  5. 令 r 为为抽象关系比较算法 rval < lval( 参见 11.8.5) 的结果,参数 LeftFirst 设为 false
  6. 如果 r 为 true 或者 undefined ,返回 false. 否则 , 返回 true.

摘自:http://yanhaijing.com/es5/#195

观察上面两个运算符的执行过程,发现只有第5步和第6步不同。而对于产生式RelationalExpression来说,我们并不关注表达式RelationalExpression和ShiftExpression的求值方式,我们只想知道它是如何比较左值lval和右值rval并且得到什么结果,即第5、6步。而这两步中,最关键的就是抽象关系比较算法了。

抽象关系比较算法

LeftFirst 标志是必须的,因为 ECMAScript 规定了表达式是从左到右顺序执行的。LeftFirst 的默认值是 true,这表明在相关的表达式中,参数 x 出现在参数 y 之前。如果 LeftFirst 值是 false,情况会相反,操作数的执行必须是先 y 后 x。这样的一个小于比较的执行步骤如下:

  1. 如果 LeftFirst 标志是 true,那么
    1. 让 px 为调用 ToPrimitive(x, hint Number) 的结果。
    2. 让 py 为调用 ToPrimitive(y, hint Number) 的结果。
  2. 否则解释执行的顺序需要反转,从而保证从左到右的执行顺序
    1. 让 py 为调用 ToPrimitive(y, hint Number) 的结果。
    2. 让 px 为调用 ToPrimitive(x, hint Number) 的结果。
  3. 如果 Type(px) 和 Type(py) 得到的结果不都是 String 类型,那么
    1. 让 nx 为调用 ToNumber(px) 的结果。因为 px 和 py 都已经是基本数据类型(primitive values 也作原始值),其执行顺序并不重要。
    2. 让 ny 为调用 ToNumber(py) 的结果。
    3. 如果 nx 是 NaN,返回 undefined
    4. 如果 ny 是 NaN,返回 undefined
    5. 如果 nx 和 ny 的数字值相同,返回 false
    6. 如果 nx 是 +0 且 ny 是 -0,返回 flase
    7. 如果 nx 是 -0 且 ny 是 +0,返回 false
    8. 如果 nx 是 +∞,返回 fasle
    9. 如果 ny 是 +∞,返回 true
    10. 如果 ny 是 -∞,返回 flase
    11. 如果 nx 是 -∞,返回 true
    12. 如果 nx 数学上的值小于 ny 数学上的值(注意这些数学值都不能是无限的且不能都为 0),返回 ture。否则返回 false。
  4. 否则,px 和 py 都是 Strings 类型
    1. 如果 py 是 px 的一个前缀,返回 false。(当字符串 q 的值可以是字符串 p 和一个其他的字符串 r 拼接而成时,字符串 p 就是 q 的前缀。注意:任何字符串都是自己的前缀,因为 r 可能是空字符串。)
    2. 如果 px 是 py 的前缀,返回 true。
    3. 让 k 成为最小的非负整数,能使得在 px 字符串中位置 k 的字符与字符串 py 字符串中位置 k 的字符不相同。(这里必须有一个 k,使得互相都不是对方的前缀)
    4. 让 m 成为字符串 px 中位置 k 的字符的编码单元值。
    5. 让 n 成为字符串 py 中位置 k 的字符的编码单元值。
    6. 如果 n<m,返回 true。否则,返回 false。

注:使用或代替的时候要注意,这里的步骤 3 和加号操作符 + 算法 (11.6.1) 的步骤 7 的区别。

注:String 类型的比较使用了其编码单元值的作为一个简单的词法表序列去比较。这里不打算使用更复杂的、语义化的字符或字符串序列,和 Unicode 规范的整理序列进行比较。因此,字符串的值和其对应的 Unicode 标准的值是不相同的。实际上,这个算法假定了所有字符串已经是正常化的格式。同时要注意,对于字符串拼接追加的字符的时候,UTF-16 编码单元值的词法表序列是不同于代码点值的序列的。

摘自:http://yanhaijing.com/es5/#197

我不得不说,这规范一丝不苟,非常仔细,仔细到让人有些混乱。

LeftFirst标记和本文关系不大,可以跳过本段。文中提到,LeftFirst是必须的,因为ECMAScript规定了表达式是从左到右顺序执行的。举个例子,在小于等于运算符产生式 RelationalExpression : RelationalExpression <= ShiftExpression的第5步,会用抽象关系比较算法比较右值rval和左值lval,这时,右值参数rval在左值参数lval的左边,如果没有LeftFirst标记控制,就会先对右值参数rval作ToPrimitive等一系列操作,违背了从左到右顺序执行的规则。

上文简单来说,就是对x和y进行toPrimitive操作,得到结果px和py。如果px和py都是字符串,就按从左到右顺序依次比较相应位置字符的字符编码值,否则就对px和py执行toNumber操作,得到结果nx和ny。如果nx或者ny是NaN,则返回undefined,否则返回两数的数值比较结果。

这里,又出现两个陌生的操作:toPrimitive和toNumber。

ToPrimitive

ToPrimitive 运算符接受一个值,和一个可选的 期望类型 作参数。ToPrimitive 运算符把其值参数转换为非对象类型。如果对象有能力被转换为不止一种原语类型,可以使用可选的 期望类型 来暗示那个类型。根据下表完成转换:

ToPrimitive转换

输入类型结果
Undefined结果等于输入的参数(不转换)。
Null结果等于输入的参数(不转换)。
Boolean结果等于输入的参数(不转换)。
Number结果等于输入的参数(不转换)。
String结果等于输入的参数(不转换)。
Object返回该对象的默认值。(调用该对象的内部方法[[DefaultValue]]一樣)。

摘自:http://yanhaijing.com/es5/#103

ToNumber

ToNumber 运算符根据下表将其参数转换为数值类型的值:

ToNumber转换

输入类型结果
UndefinedNaN
Null+0
Boolean如果参数是 true,结果为 1。如果参数是 false,此结果为 +0。
Number结果等于输入的参数(不转换)。
String参见下文的文法和注释。
Object应用下列步骤:
  1. 原始值ToPrimitive( 输入参数 , 暗示 数值类型)。
  2. 返回 ToNumber( 原始值 )。

摘自:http://yanhaijing.com/es5/#105

对于toPrimitive操作,值得一提的是数组对象。数组的valueOf方法返回该数组的原始值,即对其进行toPrimitive操作的结果。我在ECMAScript5.1规范中简单搜索了一下,没有搜到明确的说明,但是我们还是可以知道对数组进行toPrimitive操作的结果类型的,就是打印“结果值+1+2”。根据打印结果得知,结果是各元素以逗号分隔的字符串。这一点是很重要的!

对于toNumber操作,需要说一下的是对字符串类型应用toNumber的情况。在转换的过程中,空白字符是可以忽略不计的,而且空串的转换结果是0。这一点也很重要!至于详细说明,可参阅:http://yanhaijing.com/es5/#106

到此为止,我们只是完成了JavaScript的比较运算。下面,来分析一下等值运算。别着急,快结束了。

对于等值运算,我们只需分析一下==即可,对==的结果取反后就是!=的结果。

The Equals Operator ( == )

产生式 EqualityExpression : EqualityExpression == RelationalExpression 按照下面的过程执行 :

  1. 令 lref 为解释执行 EqualityExpression 的结果 .
  2. 令 lval 为 GetValue(lref).
  3. 令 rref 为解释执行 RelationalExpression 的结果 .
  4. 令 rval 为 GetValue(rref).
  5. 返回作用抽象相等比较算法于 rval == lval( 参见 11.9.3) 的结果

摘自:http://yanhaijing.com/es5/#201

我们直接看第5步,这里涉及到了抽象相等比较算法。

抽象相等比较算法

比较运算x==y, 其中xy是值,产生true或者false。这样的比较按如下方式进行:

  1. 若Type(x)与Type(y)相同, 则若x为null且y为undefined, 返回true。
    1. 若Type(x)为Undefined, 返回true。
    2. 若Type(x)为Null, 返回true。
    3. 若Type(x)为Number, 则
      1. 若x为NaN, 返回false。
      2. 若y为NaN, 返回false。
      3. 若x与y为相等数值, 返回true。
      4. 若x 为 +0 且 y为−0, 返回true。
      5. 若x 为 −0 且 y为+0, 返回true。
      6. 返回false。
    4. 若Type(x)为String, 则当x和y为完全相同的字符序列(长度相等且相同字符在相同位置)时返回true。 否则, 返回false。
    5. 若Type(x)为Boolean, 当x和y为同为true或者同为false时返回true。 否则, 返回false。
    6. 当x和y为引用同一对象时返回true。否则,返回false。
  2. 若x为null且y为undefined, 返回true。
  3. 若x为undefined且y为null, 返回true。
  4. 若Type(x) 为 Number 且 Type(y)为String, 返回comparison x == ToNumber(y)的结果。
  5. 若Type(x) 为 String 且 Type(y)为Number,返回比较ToNumber(x) == y的结果。
  6. 若Type(x)为Boolean, 返回比较ToNumber(x) == y的结果。
  7. 若Type(y)为Boolean, 返回比较x == ToNumber(y)的结果。
  8. 若Type(x)为String或Number,且Type(y)为Object,返回比较x == ToPrimitive(y)的结果。
  9. 若Type(x)为Object且Type(y)为String或Number, 返回比较ToPrimitive(x) == y的结果。
  10. 返回false。

注:按以上相等之定义:

  • 字符串比较可以按这种方式强制执行: "" + a == "" + b
  • 数值比较可以按这种方式强制执行: +a == +b
  • 布尔值比较可以按这种方式强制执行: !a == !b

注:等值比较操作保证以下不变:

  • A != B等价于!(A==B)
  • A == B等价于B == A,除了A与B的执行顺序。

注:相等运算符不总是传递的。例如,两个不同的String对象,都表示相同的字符串值;==运算符认为每个String对象都与字符串值相等,但是两个字符串对象互不相等。例如:

  • new String("a") == "a""a" == new String("a")皆为true。
  • new String("a")==new String("a")为false。

注:字符串比较使用的方式是简单地检测字符编码单元序列是否相同。不会做更复杂的、基于语义的字符或者字符串相等的定义以及Unicode规范中定义的 collating order。所以Unicode标准中认为相等的String值可能被检测为不等。实际上这一算法认为两个字符串已经是经过规范化的形式。

摘自:http://yanhaijing.com/es5/#203

有句话叫做过尤不及,严格的过了头儿,也是不好的。这规范就有这毛病,凑合着看吧。

上文简单的说,就是如果x和y类型相同,等值比较比较简单,只需要注意两点:1、当x和y都是Number类型时,若x或者y是NaN,则返回false;2、当x和y都是Object对象时,需看两者是不是引用的同一对象。如果x和y类型不同,那就费点事了:1、undefined == null成立,除此之外,undefined或者null和其它类型数据比较都返回false;2、数字和字符串作等值比较时,需把字符串转换成数字后再作比较;3、布尔值在和其它数据作等值比较时,需把布尔值转换成数字后再做比较;4、对象类型数据在和其它数据作比较时,需先获取对象的原始值,然后再做比较;其它情况返回false。

好了,JavaScript的等值运算我们也分析完了。只差if语句了,加油!

if语句判定真假时需对表达式作toBoolean操作,参阅:http://yanhaijing.com/es5/#220

ToBoolean

ToBoolean 运算符根据下表将其参数转换为布尔值类型的值:

ToBoolean转换

输入类型结果
Undefinedfalse
Nullfalse
Boolean结果等于输入的参数(不转换)。
Number如果参数是 +0, -0, 或 NaN,结果为 false ;否则结果为 true。
String如果参数参数是空字符串(其长度为零),结果为 false,否则结果为 true。
Objecttrue

摘自:http://yanhaijing.com/es5/#104

这个没什么好说的,符合程序员的逻辑思维。如果在你的思维里不是这样的,那么只需要记住就行了。

到现在为止,终于可以画上句号了。如果你的大脑还没有崩溃,那么,请继续做一做下面的题吧,看你能对几个。


  1. undefined <= 0, undefined == 0
  2. null <= 0, null == 0
  3. undefined <= null, undefined == null
  4. '' <= 0, '' == 0, if ('')
  5. ' ' <= 0, ' ' == 0, if (' ')
  6. '0' <= 0, '0' == 0
  7. NaN <= 0, NaN == 0, NaN >= 0
  8. '2' <= true, '2' == true
  9. '0' <= false, '0' == false, if ('0')
  10. [] == 0, [0] == 0, ['0'] == 0
  11. if ([]), if ([0])
  12. {} >= 0, {} == 0

答案:

  1. false, false
  2. true, false
  3. false, true
  4. true, true, false
  5. true, true, true
  6. true, true
  7. false, false, false
  8. false, false
  9. true, true, true
  10. true, true, true
  11. true, true
  12. false, false

好了,我的大脑已经崩溃,剩下的就靠你们自己了。如果你们还有精力,建议你们再去分析一下PHP的相关逻辑。相信你们!