返回介绍

第一部分 类型和语法

第二部分 异步和性能

5.2 运算符优先级

发布于 2023-05-24 16:38:21 字数 7062 浏览 0 评论 0 收藏 0

第 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 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文