1.1 || 和&&
逻辑运算符||(或)和&&(与)应该并不陌生,也许正因为如此有人觉得它们在 JavaScript 中的表现也和在其他语言中一样。
这里面有一些非常重要但却不太为人所知的细微差别。
我其实不太赞同将它们称为“逻辑运算符”,因为这不太准确。称它们为“选择器运算符 ”( selector operators)或者“操作数选择器运算符”(operand selector operators)更恰当些。
为什么?因为和其他语言不同,在JavaScript中它们返回的并不是布尔值。 它们的返回值是两个操作数中的一个(且仅一个)。即选择两个操作数中的一个,然后返回它的值。
&& 和||运算符的返回值并不一定是布尔类型,而是两个操作数其中一个的值。
例如:
var a = 42;
var b = "abc";
var c = null;
a || b; // 42
a && b; // "abc"
c || b; // "abc"
c && b; // null
在C和PHP中,上例的结果是true或false,在JavaScript(以及Python和Ruby)中却是某个操作数的值。
|| 和&&首先会对第一个操作数(a和c)执行条件判断,如果其不是布尔值(如上例)就 先进行ToBoolean强制类型转换,然后再执行条件判断。
对于||来说,如果条件判断结果为true就返回第一个操作数(a和c)的值,如果为 false 就返回第二个操作数(b)的值。
&& 则相反,如果条件判断结果为true就返回第二个操作数(b)的值,如果为false就返 回第一个操作数(a和c)的值。
|| 和&&返回它们其中一个操作数的值,而非条件判断的结果(其中可能涉及强制类型转 换 )。 c && b中c为null,是一个假值,因此&&表达式的结果是null(即c的值),而非条件判断的结果false。
现在明白我为什么把它们叫作“操作数选择器”了吧?
换一个角度来理解:
a || b;
// 大致相当于(roughly equivalent to):
a ? a : b;
a && b;
// 大致相当于(roughly equivalent to):
a ? b : a;
之所以说大致相当,是因为它们返回结果虽然相同但是却有一个细微的差 别。在a ? a : b中,如果a是一个复杂一些的表达式(比如有副作用的函 数调用等),它有可能被执行两次(如果第一次结果为真)。而在a || b中a 只执行一次,其结果用于条件判断和返回结果(如果适用的话)。a b和a ? b : a 也是如此。
下面是一个十分常见的||的用法,也许你已经用过但并未完全理解:
function foo(a,b) {
a = a || "hello";
b = b || "world";
console.log( a + " " + b );
}
foo(); // "hello world"
foo( "yeah", "yeah!" ); // "yeah yeah!"
a = a || "hello"(又称为 C# 的“空值合并运算符”的JavaScript版本)检查变量a,如果还未赋值(或者为假值),就赋予它一个默认值("hello")。 这里需要注意!
foo( "That’s it!", "" ); // "That’s it! world" <-- 晕!
第二个参数""是一个假值(falsy value,参见4.2.3节),因此b = b || "world"条件不成立,返回默认值"world"。
这种用法很常见,但是其中不能有假值,除非加上更明确的条件判断,或者转而使用? : 三元表达式。
通过这种方式来设置默认值很方便,甚至那些公开诟病JavaScript强制类型转换的人也经常使用。 再来看看&&。
有一种用法对开发人员不常见,然而JavaScript代码压缩工具常用。就是如果第一个操作数为真值,则&&运算符“选择”第二个操作数作为返回值,这也叫作“守护运算符” (guard operator,参见 5.2.1 节),即前面的表达式为后面的表达式“把关”:
function foo() {
console.log( a );
}
var a = 42;
a && foo(); // 42
foo() 只有在条件判断a通过时才会被调用。如果条件判断未通过,a && foo()就会悄然 终止(也叫作“短路”,short circuiting), foo() 不会被调用。
这样的用法对开发人员不太常见,开发人员通常使用if (a) { foo(); }。但JavaScript 代码压缩工具用的是a && foo(),因为更简洁。以后再碰到这样的代码你就知道是怎么 回事了。
|| 和&&各自有它们的用武之地,前提是我们理解并且愿意在代码中运用隐式强制类型转换。
你大概会有疑问:既然返回的不是true和false,为什么a && (b || c)这样的表达式在 if 和for中没出过问题?
这或许并不是代码的问题,问题在于你可能不知道这些条件判断表达式最后还会执行布尔值的隐式强制类型转换(我们之前有详细介绍过--->关于变量类型转换的讨论)
1.2 运算符优先级
上一节中介绍过,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运算符优先级有深入了解的 开发人员并不多。
如果我们明白其中的道理,上面的例子就是小菜一碟。不过估计很多读者看到上面几个例子时还是需要细细琢磨一番。
1.2.1 短路
对 && 和 || 来说,如果从左边的操作数能够得出结果,就可以忽略右边的操作数。我们将这种现象称为“短路”(即执行最短路径)。
以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()函数,这样可以避 免执行不必要的代码。
1.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..)。
1.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
这里我们可以看出,? :是右关联,并且它的组合方式会影响返回结果。