5.2 运算符优先级
第 4 章中介绍过,JavaScript 中的 && 和 || 运算符返回它们其中一个操作数的值,而非 true 或 false 。在一个运算符两个操作数的情况下这比较好理解:
var a = 42; var b = "foo"; a && b; // "foo" a || b; // 42
那么两个运算符三个操作数呢?
var a = 42; var b = "foo"; var c = [1,2,3]; a && b || c; // ??? a || b && c; // ???
想知道结果就需要了解超过一个运算符时表达式的执行顺序。
这些规则被称为“运算符优先级”(operator precedence)。
估计大多数读者都会认为自己已经掌握了运算符优先级。这里我们秉承本系列丛书的一贯宗旨,将对这个主题进行深入探讨,希望读者从中能有新的收获。
回顾前面的例子:
var a = 42, b; b = ( a++, a ); a; // 43 b; // 43
如果去掉 ( ) 会出现什么情况?
var a = 42, b; b = a++, a; a; // 43 b; // 42
为什么上面两个例子中 b 的值会不一样?
原因是 , 运算符的优先级比 = 低。所以 b = a++, a 其实可以理解为 (b = a++), a 。前面说过 a++ 有后续副作用 (after side effect),所以 b 的值是 ++ 对 a 做递增之前的值 42 。
这只是一个简单的例子。请务必记住,用 , 来连接一系列语句的时候,它的优先级最低,其他操作数的优先级都比它高。
回顾前面的一个例子:
if (str && (matches = str.match( /[aeiou]/g ))) { // .. }
这里对赋值语句使用 ( ) 是必要的,因为 && 运算符的优先级高于 = ,如果没有 ( ) 对其中的表达式进行绑定(bind)的话,就会执行作 (str && matches) = str.match.. 。这样会出错,由于 (str && matches) 的结果并不是一个变量,而是一个 undefined 值,因此它不能出现在 = 运算符的左边!
下面再来看一个更复杂的例子(本章后面几节都将用到):
var a = 42; var b = "foo"; var c = false; var d = a && b || c ? c || b ? a : c && b : a; d; // ??
应该没有人会写出这样恐怖的代码,这只是用来举例说明多个运算符串联时可能出现的一些常见问题。
上例的结果是 42 ,当然只要运行一下代码就能够知道答案,但是弄明白其中的来龙去脉更有意思。
首先我们要搞清楚 (a && b || c) 执行的是 (a && b) || c 还是 a && (b || c) ?它们之间有什么区别?
(false && true) || true; // true false && (true || true); // false
事实证明它们是有区别的,false && true || true 的执行顺序如下:
false && true || true; // true (false && true) || true; // true
&& 先执行,然后是 || 。
那执行顺序是否就一定是从左到右呢?不妨将运算符颠倒一下看看:
true || false && false; // true (true || false) && false; // false true || (false && false); // true
这说明 && 运算符先于 || 执行,而且执行顺序并非我们所设想的从左到右。原因就在于运算符优先级 。
每门语言都有自己的运算符优先级。遗憾的是,对 JavaScript 运算符优先级有深入了解的开发人员并不多。
如果我们明白其中的道理,上面的例子就是小菜一碟。不过估计很多读者看到上面几个例子时还是需要细细琢磨一番。
遗憾的是,JavaScript 规范对运算符优先级并没有一个集中的介绍,因此我们需要从语法规则中间逐一了解。下面列出一些常见并且有用的优先级规则,以方便查阅。完整列表请参见 MDN 网站(https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Operator_Precedence )上的“优先级列表”。
5.2.1 短路
第 4 章中的附注栏提到过 && 和 || 运算符的“短路”(short circuiting)特性。下面我们将对此进行详细介绍。
对 && 和 || 来说,如果从左边的操作数能够得出结果,就可以忽略右边的操作数。我们将这种现象称为“短路”(即执行最短路径)。
以 a && b 为例,如果 a 是一个假值,足以决定 && 的结果,就没有必要再判断 b 的值。同样对于 a || b ,如果 a 是一个真值,也足以决定 || 的结果,也就没有必要再判断 b 的值。
“短路”很方便,也很常用,如:
function doSomething(opts) { if (opts && opts.cool) { // .. } }
opts && opts.cool 中的 opts 条件判断如同一道安全保护,因为如果 opts 未赋值(或者不是一个对象),表达式 opts.cool 会出错。通过使用短路特性,opts 条件判断未通过时 opts.cool 就不会执行,也就不会产生错误!
|| 运算符也一样:
function doSomething(opts) { if (opts.cache || primeCache()) { // .. } }
这里首先判断 opts.cache 是否存在,如果是则无需调用 primeCache() 函数,这样可以避免执行不必要的代码。
5.2.2 更强的绑定
回顾一下前面多个运算符串联在一起的例子:
a && b || c ? c || b ? a : c && b : a
其中 ? : 运算符的优先级比 && 和 || 高还是低呢?执行顺序是这样?
a && b || (c ? c || (b ? a : c) && b : a)
还是这样?
(a && b || c) ? (c || b) ? a : (c && b) : a
答案是后者。因为 && 运算符的优先级高于 || ,而 || 的优先级又高于 ? : 。
因此表达式 (a && b || c) 先于包含它的 ? : 运算符执行。另一种说法是 && 和 || 比 ? : 的绑定更强。反过来,如果 c ? c... 的绑定更强,执行顺序就会变成 a && b || (c ? c..) 。
5.2.3 关联
&& 和 || 运算符先于 ? : 执行,那么如果多个相同优先级的运算符同时出现,又该如何处理呢?它们的执行顺序是从左到右还是从右到左?
一般说来,运算符的关联(associativity)不是从左到右就是从右到左,这取决于组合(grouping)是从左开始还是从右开始。
请注意:关联和执行顺序不是 一回事。
但它为什么又和执行顺序相关呢?原因是表达式可能会产生副作用,比如函数调用:
var a = foo() && bar();
这里 foo() 首先执行,它的返回结果决定了 bar() 是否执行。所以如果 bar() 在 foo() 之前执行,整个结果会完全不同。
这里遵循从左到右的顺序(JavaScript 的默认执行顺序),与 && 的关联无关。因为上例中只有一个 && 运算符,所以不涉及组合和关联。
而 a && b && c 这样的表达式就涉及组合(隐式),这意味着 a && b 或 b && c 会先执行。
从技术角度来说,因为 && 运算符是左关联(|| 也是),所以 a && b && c 会被处理为 (a && b) && c 。不过右关联 a && (b && c) 的结果也一样。
如果 && 是右关联的话会被处理为 a && (b && c) 。但这并不意味着 c 会在 b 之前执行。右关联不是指从右往左执行,而是指从右往左组合 。任何时候,不论是组合还是关联,严格的执行顺序都应该是从左到右,a ,b ,然后 c 。
所以,&& 和 || 运算符是不是左关联这个问题本身并不重要,只要对此有一个准确的定义即可。
但情况并非总是这样。一些运算符在左关联和右关联时的表现截然不同。
比如 ? : (即三元运算符或者条件运算符):
a ? b : c ? d : e;
? : 是右关联,它的组合顺序是以下哪一种呢?
· a ? b : (c ? d : e)
· (a ? b : c) ? d : e
答案是 a ? b : (c ? d : e) 。和 && 以及 || 运算符不同,右关联在这里会影响返回结果,因为 (a ? b : c) ? d : e 对有些值(并非所有值)的处理方式会有所不同。
举个例子:
true ? false : true ? true : true; // false true ? false : (true ? true : true); // false (true ? false : true) ? true : true; // true
在某些情况下,返回的结果没有区别,但其中却有十分微妙的差别。例如:
true ? false : true ? true : false; // false true ? false : (true ? true : false); // false (true ? false : true) ? true : false; // false
这里返回的结果一样,运算符组合看似没起什么作用。然而实际情况是:
var a = true, b = false, c = true, d = true, e = false; a ? b : (c ? d : e); // false, 执行 a 和 b (a ? b : c) ? d : e; // false, 执行 a, b 和 e
这里我们可以看出,? : 是右关联,并且它的组合方式会影响返回结果。
另一个右关联(组合)的例子是 = 运算符。本章前面介绍过一个串联赋值的例子:
var a, b, c; a = b = c = 42;
它首先执行 c = 42 ,然后是 b = .. ,最后是 a = .. 。因为是右关联,所以它实际上是这样来处理的:a = (b = (c = 42)) 。
再看看本章前面那个更为复杂的赋值表达式的例子:
var a = 42; var b = "foo"; var c = false; var d = a && b || c ? c || b ? a : c && b : a; d; // 42
掌握了优先级和关联等相关知识之后,就能够根据组合规则将上面的代码分解如下:
((a && b) || c) ? ((c || b) ? a : (c && b)) : a
也可以通过缩进显式让代码更容易理解:
( (a && b) || c ) ? ( (c || b) ? a : (c && b) ) : a
现在来逐一执行。
(1) (a && b) 结果为 "foo" 。
(2) "foo" || c 结果为 "foo" 。
(3) 第一个 ? 中,"foo" 为真值。
(4) (c || b) 结果为 "foo" 。
(5) 第二个 ? 中,"foo" 为真值。
(6) a 的值为 42 。
因此,最后结果为 42 。
5.2.4 释疑
现在你应该对运算符优先级(和关联)有了更深入的了解,多个运算符串联的代码也不在话下。
但是我们仍然面临着一个重要问题,即是不是理解和遵守了运算符优先级和关联规则就万事大吉了?在必要时是否应该使用 ( ) 来自行控制运算符的组合和执行顺序?
换句话说,尽管这些规则是可以学习和掌握的,但其中也不乏问题和陷阱。如果完全依赖它们来编码,就很容易掉进陷阱。那么是否应该经常使用 ( ) 来自行控制运算符的执行而不再依赖系统的自动操作呢?
正如第 4 章中的隐式 强制类型转换,这个问题仁者见仁,智者见智。对于两者,大多数人的看法都是:要么完全依赖规则编码,要么完全使用显式和自行控制的方式。
对于这个问题,我并没有一个明确的答案。它们各自的优缺点本书都已予以介绍,希望能有助于你加深理解,从而做出自己的判断。
我认为,针对该问题有个折中之策,即在编写程序时要将两者结合起来,既要依赖运算符优先级 / 关联规则,也要适当使用 ( ) 自行控制方式。对第 4 章中的隐式 强制类型转换也是如此,我们应该安全合理地运用它们,而非无节制地滥用。
例如,如果 if (a && b && c) .. 没问题,我就不会使用 if ((a && b) && c) .. ,因为这样过于繁琐。
然而,如果需要串联两个 ? : 运算符的话,我就会使用 ( ) 来自行控制运算符的组合,让代码更清晰易读。
所以我的建议和第 4 章中一样:如果运算符优先级 / 关联规则能够令代码更为简洁,就使用运算符优先级 / 关联规则;而如果 ( ) 有助于提高代码可读性,就使用 ( ) 。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论