JavaScript 是否具有“短路”求值?

147
我想知道JavaScript是否有像C#中的“短路”评估一样的&&运算符。如果没有,我想知道是否有一种解决方法是有意义的采用。

2
不用谢。我已经添加了 https://www.google.com/search?q=site:stackoverflow.com+%s 作为搜索快捷方式(Chrome/Firefox),以加快搜索速度。 - Rob W
这里也有我问题的答案:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/Logical_Operators - GibboK
更多有用的资源:The || evaluation question The && evaluation question - Samuel Hulla
紧密相关:JavaScript中的逻辑运算符-如何使用? - Sebastian Simon
3个回答

161

是的,JavaScript有"短路"求值。

if (true == true || foo.foo){
    // Passes, no errors because foo isn't defined.
}

现场演示

if (false && foo.foo){
    // Passes, no errors because foo isn't defined.
}

实时演示


10
短路运算在JavaScript中是标准用法吗? - GibboK
1
谢谢gdoron,能帮我理解一下C#中的...吗?我也有像&这样的二进制运算符,所以两个操作数都必须为真才能通过,而使用C中的&&则不同。 - GibboK
1
@GibboK。显然,使用该逻辑运算符时就不可能出现“短路”。你可以自己试试,使用我的演示程序。 - gdoron
2
@GibboK:看看这个运算符参考。是的,在JS中也有二进制AND运算符。 - Bergi
8
在标准中是这样的!但是评论很好,因为在JavaScript实现中进行JIT编译魔法时,人们确实想知道某些东西是否符合“标准”,或者可能受到实现的影响。二进制逻辑运算符条件语句的求值方式和(短路)是一种标准行为。http://www.ecma-international.org/ecma-262/5.1/#sec-11.11 - humanityANDpeace
显示剩余6条评论

59
答案是肯定的!

本答案详细介绍了JavaScript中的工作原理,包括所有需要注意的问题以及相关主题,例如操作符优先级。 如果你想要快速了解定义并已经了解短路求值的工作原理,我建议查看其他答案。


到目前为止,我们(认为我们)已知的内容:

首先让我们检查我们都熟悉的行为,在 if 条件语句中,我们使用 && 来检查两个表达式是否都为 true

const expr1 = true;
const expr2 = true;

if (expr1 && expr2) {
  console.log("bar");
}

现在,你的第一反应可能是说:“啊,是的,非常简单,如果expr1expr2都被评估为true,那么代码将执行语句”
嗯,是的和不是。从技术上讲,你是正确的,这就是你描述的行为,但这并不是代码的实际评估方式,我们需要深入探讨才能完全理解。 &&||究竟如何解释?
现在是时候看看引擎的内部了。让我们考虑这个实际的例子:

function sanitize(x) {
  if (isNaN(x)) {
    return NaN;
  }

  return x;
}

let userInput = 0xFF; // As an example.
const res = sanitize(userInput) && userInput + 5

console.log(res);

好的,结果是260,但为什么呢? 为了得到答案,我们需要了解短路求值的工作原理。

根据MDN定义&&运算符在expr1 && expr2中的执行方式如下:

逻辑与运算符(&&)从左到右评估操作数,遇到第一个falsy操作数立即返回其值;如果所有值都是truthy,则返回最后一个操作数的值。 如果一个值可以转换为true,那么这个值就被称为truthy。 如果一个值可以转换为false,那么这个值就被称为falsy。 [...] 由于每个操作数都被转换为布尔值,如果一个转换的结果为false,则AND运算符将停止并返回该falsy操作数的原始值;它不会评估其余的操作数。

或者更简单地说,在文档的旧版本中:

运算符 语法 描述
逻辑 AND (&&) expr1 && expr2 如果expr1可以转换为true,则返回expr2; 否则返回expr1

因此,这意味着在我们的实际示例中,const res 的计算方式如下:

  1. 调用expr1,它是sanitize(userInput)sanitize(0xFF)
  2. 运行sanitize(0xFF):检查isNaN(x)isNaN(0xFF)(结果为false,因为0xFF是255的有效十六进制数值),返回x,即0xFF255。如果isNaN(x)truesanitize将返回NaN
  3. expr1的结果为255,是“truthy”值,所以现在要评估expr2。如果返回NaN,它将停止,因为NaN是falsy。
  4. 由于sanitize(userInput)是“truthy”(一个非零有限数字),继续并将5添加到userInput中。
“Truthy” 意味着表达式可以被评估为真。
因此,在这里,我们能够通过简单地使用 && 运算符来避免额外的 if 块和进一步的 isNaN 检查。
它的实际作用是:
到现在为止,我们应该至少有一个图片, 运算符如何工作。
表现出短路行为的运算符是:

通用规则是:

  • a && b &&&& z 的计算结果为第一个假值操作数,如果没有假值操作数,则结果为最后一个操作数。
  • a || b |||| z 的计算结果为第一个真值操作数,如果没有真值操作数,则结果为最后一个操作数。
  • a ?? b ???? z 的计算结果为第一个既不是null也不是undefined的操作数,如果没有这样的操作数,则结果为最后一个操作数。
  • a?.b?.?.z 访问链中的每个属性并计算嵌套z属性的值,如果链的所有先前链接都计算为可以转换为对象(即既不是null也不是undefined),则返回嵌套属性的值;否则,无论哪个嵌套属性失败,都将返回undefined

以下是一些更好理解的例子:

function a() {
  console.log("a");

  return false;
}

function b() {
  console.log("b");

  return true;
}

if (a() && b()){
   console.log("foobar"); 
}

// `a()` is evaluated as `false`; execution is stopped.

function a() {
  console.log("a");

  return false;
}

function b() {
  console.log("b");

  return true;
}

if (a() || b()){
   console.log("foobar"); 
}

/*
** 1. `a()` is evaluated as `false`.
** 2. So it should evaluate `expr2`, which is `b()`.
** 3. `b()` is evaluated `true`.
** 4. The statement `console.log("foobar");` is executed.
*/

function a() {
  console.log("a");

  return null;
}

function b() {
  console.log("b");

  return false;
}

function c() {
  console.log("c");

  return true;
}

if (a() ?? b() ?? c()){
   console.log("foobar"); 
}

/*
** 1. `a()` is evaluated as `null`.
** 2. So it should evaluate `expr2`, which is `b()`.
** 3. `b()` is evaluated as `false`; execution is stopped.
*/

const deeply = {
    get nested(){
      console.log("nested");
      
      return {
        get object(){
          console.log("object");
          
          return null;
        }
      };
    }
  };

if (deeply?.nested?.object?.but?.not?.that?.deep){
   console.log("foobar"); 
}

/*
** 1. `deeply` is evaluated as an object.
** 2. `deeply?.nested` is evaluated as an object.
** 3. `deeply?.nested?.object` is evaluated as `null`.
** 4. `?.but?.not?.that?.deep` is essentially skipped over; the entire optional chain is evaluated as `undefined`; execution is stopped.
*/


最后一个麻烦但非常重要的事情:运算符优先级

很好,希望你已经掌握了! 我们需要知道的最后一件事是关于运算符优先级的规则,即:&& 运算符总是在 || 运算符之前进行计算。

考虑以下示例:

function a() { console.log("a"); return true;}
function b() { console.log("b"); return false;}
function c() { console.log("c"); return false;}

console.log(a() || b() && c());

// "a" is logged; execution is stopped.

表达式a() || b() && c()可能会让一些人感到困惑,其结果实际上是a()。 原因很简单,只是我们的视觉有点欺骗我们,因为我们习惯从左到右阅读。 让我们把console.log()之类的东西拿掉,专注于评估:
true || false && false

&&||的优先级更高,这意味着最靠近&&的操作数首先被计算,但在已经计算了更高优先级的所有操作之后才会计算。由于这里只有&&||,因此它们都是false

||也是同样的规则,包括所有优先级更高的操作都应该已经被计算的规则。

  1. 所以,首先我们需要计算false && false,这只是false
  2. 然后我们计算true || false(结果为false),这是true
  3. 整个表达式等价于(true || (false && false)),这是(true || (false)),这是(true)
你也可以尝试另一种角度:在ECMAScript规范中(JavaScript语言基于此),表达式expr1 || expr2遵循一种称为LogicalORExpression的模式。 根据定义,简单来说,expr1expr2都是它们自己的LogicalANDExpression
  1. 如果您想评估类似true || false && false这样的内容,则需要评估LogicalORExpression:(true) || (false && false)。 您知道(true)只是true,但您不会立即知道(false && false)是什么。
  2. 然后,您需要评估LogicalANDExpression:(false) && (false)。 现在您完成了,因为(false)只是false
只有在评估每个LogicalANDExpression的结果之后,您才能继续评估组成的LogicalORExpression。 这正是&&||之前被评估或&&优先级高于||的含义。
(注:这些语法规则一举两得:它们通过递归定义定义了评估顺序,并定义了运算符优先级(在此情况下为从左到右和&&||之前))。
嗯,这可能看起来非常棘手,所有这些都是由于一些奇怪的规则和语义。 但是请记住,您始终可以使用括号(也称为分组运算符())来避免运算符优先级 - 就像在数学中一样。

function a() { console.log("a"); return true; }
function b() { console.log("b"); return false; }
function c() { console.log("c"); return false; }

console.log((a() || b()) && c());

/*
** 1. The parenthesized part is evaluated first.
** 2. `a()` is evaluated as `true`, so `b()` is skipped
** 3. `c()` is evaluated as `false`, stops execution.
*/

我们还没有讨论在优先级方面放置??运算符的位置!但是不用担心:由于&&||以及??之间的运算符优先级规则会太混乱和复杂,因此实际上不允许将它们放在一起!只有当非常清楚哪个先被评估时,它们才能在同一个表达式中出现。

(a ?? b) && c  // Clearly, `(a ?? b)` is evaluated first.
a ?? (b && c)  // Clearly, `(b && c)` is evaluated first.
a ?? b && c    // Unclear! Throws a SyntaxError.

1
这是正确的答案,附有深入的解释,应该被标记为已接受并获得更多的赞同! - Alexander Kim
精彩的解释。唯一不清楚的部分是运算符优先级的最后一个示例。 - Peter Out
1
我已经编辑过了,希望解决了所有评论中提到的问题:更好的变量名,明确的答案,正确的十六进制数值,正确的优先级解释。还更新了 MDN 链接和引用,提供了更准确的措辞和更深入的优先级视图(加上了一些小的编辑更改)。最重要的是,这个答案终于提到了可选链 (?.) 和空值合并 (??) 运算符!老实说,即使 &&=, ||=, 和 ??= 没有被提及,这个答案现在感觉有点过多;也许关于优先级的细节并不需要那么多。 - Sebastian Simon
@SebastianSimon 非常感谢,自从回答发布以来已经有一段时间了,但很遗憾我一直没有时间更新它。但我只是想过来表达我的感激之情。是的,它几乎成为了一个MDN页面,但我仍然认为在原始帖子和多年来的精彩更新中,它已经成为了互联网上关于JS短链接的最佳且简明的答案之一 :-) - Samuel Hulla

1
这个想法是逻辑表达式从左到右读取,如果左侧条件的值足以得到总值,则不会处理和评估右侧条件。以下是一些非常简单的例子:

function test() {
    const caseNumber = document.querySelector('#sel').value;
    const userChoice = () => confirm('Press OK or Cancel');
    if (caseNumber === '1') {
        console.log (1 === 1 || userChoice());
    } else if (caseNumber === '2') {
        console.log (1 === 2 && userChoice());
    } else if (caseNumber === '3') {
        console.log (1 === 2 || userChoice());
    } else if (caseNumber === '4') {
        console.log (1 === 1 && userChoice());
    } else if (caseNumber === '5') {
        console.log (userChoice() || 1 === 1);
    } else if (caseNumber === '6') {
        console.log (userChoice() && 1 === 2);
    }
}
<label for="sel">Select a number of a test case and press "RUN!":</label>
<br><select id="sel">
  <option value="">Unselected</option>
  <option value="1">Case 1</option>
  <option value="2">Case 2</option>
  <option value="3">Case 3</option>
  <option value="4">Case 4</option>
  <option value="5">Case 5</option>
  <option value="6">Case 6</option>
</select>
<button onclick="test()">RUN!</button>

以上前两种情况将分别打印出控制台结果truefalse,您甚至不会看到模态窗口询问您是否按“确定”或“取消”,因为左侧条件已足以定义总体结果。 相反,对于第3-6种情况,您将看到模态窗口询问您的选择,因为前两种情况取决于右侧部分(即您的选择),而后两种情况——尽管这些表达式的聚合值不取决于您的选择——因为左侧条件首先被读取。因此,根据您想要首先处理哪些条件,将条件从左到右放置非常重要。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接