共计 5313 个字符,预计需要花费 14 分钟才能阅读完成。
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
这里我们可以看出,? : 是右关联,并且它的组合方式会影响返回结果。
原文地址: JS 深入 05 关于 || 和 &&,== 优先级的解析