JavaScript 有“短路”评估吗?

我想知道 JavaScript 是否像 C # 中的 & & Operator 那样有“短路”计算。如果没有,我想知道是否有一个解决办法,有意义的采用。

65460 次浏览

是的,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.
}

现场演示

答案是肯定的!

这个答案详细介绍了 在 JavaScript 中是如何工作的,包括所有的陷阱和相关的主题,比如操作符优先级。 如果你正在寻找一个快速的定义并且已经理解了短路是如何工作的,我建议你检查一下其他的答案。


到目前为止我们(以为)所知道的:

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

const expr1 = true;
const expr2 = true;


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

现在,你的第一反应可能是说: “啊,是的,非常简单,如果 ABC0和 ABC1都计算为 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中的 &&操作员执行以下操作:

逻辑 AND (&&)从左到右计算操作数,立即返回它遇到的第一个 假的操作数的值; 如果所有值都是 真心话,则返回最后一个操作数的值。

如果一个值可以转换成 true,那么这个值就是所谓的 真心话。 如果一个值可以转换成 false,那么这个值就是所谓的 假的

[...]

当每个操作数转换为布尔值时,如果发现一个转换的结果是 false,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,即 0xFF,或者 255。如果 isNaN(x)trueisNaN(x)0就会返回 isNaN(x)1。
  3. expr1的结果是 255,一个“真实”的值,所以现在是评估 expr2的时候了。如果返回的是 NaN,那就是 别说了,因为 NaN是假的。
  4. 因为 sanitize(userInput)是真实的(一个非零的有限数字) ,所以继续向前,将 5加到 userInput

“ Truthy”意味着表达式可以被评估为 true。

因此,在这里,我们可以通过简单地使用 &&操作符来避免额外的 if块和进一步的 isNaN检查。

如何运作:

到目前为止,我们至少应该知道 操作员是如何工作的。

表现出短路行为的操作员有:

普遍规律是:

  • && z计算为 第一个冒牌货操作数,如果没有发现假操作数,则计算为 最后操作数。
  • || z计算为 第一个真相操作数,如果没有找到真实的操作数,则计算为 最后操作数。
  • ?? 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.
*/


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

很好,希望你能找到窍门! 我们需要知道的最后一件事是关于操作符优先级的规则,即: < strong > &&操作符总是在 ||操作符之前求值。

考虑下面的例子:

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

优先级高于 ||&&意味着首先计算 最接近 &&的操作数,但是 之后所有优先级高于 甚至更高的操作都已经计算过了。 因为这里只有 &&||这里只有 falsefalse

||的工作方式相同,包括所有具有 甚至更高优先级的操作都应该已经被计算的规则。

  1. 所以 第一,我们需要计算 false && false,也就是 false
  2. 然后评估 true || false(使用 结果 false) ,即 true
  3. 整个表达式等价于 (true || (false && false)),即 (true || (false)),即 (true)

您还可以尝试另一种视角: 在 ECMAScript 规范(这是 JavaScript 语言所基于的)中,表达式 expr1 || expr2遵循一种称为 逻辑表达式的模式。 根据它的定义,简单地说,expr1expr2都是它们自己的 逻辑与表达式

  1. 如果你想评估类似 true || false && false的东西,你必须评估 逻辑表达式: (true) ||(false && false)。 你知道(true)只是 true,但是你不能立即知道(false && false)是什么。
  2. 那么,你必须评估 逻辑与表达式: (false) &&(false)。 现在就完成了,因为(false)就是 false

只有当您知道评估每个 逻辑与表达式的结果之后,您才能继续评估构成 逻辑表达式的结果。 这正是 &&||之前被评估的意思,或者 &&的优先级高于 ||

(注意: 这些 语法规则一石二鸟: 它们定义了评估顺序(通过递归定义) 和 < em > 操作符优先级(在这种情况下: 从左到右,||之前的 &&)。)

这看起来挺棘手的,就因为一些奇怪的规则和语义。 但是请记住,您总是可以使用括号(也称为 分组操作符 ABC0... )就像数学一样)来转义运算符优先级。

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.

其思想是从左到右读取逻辑表达式,如果左边条件的值足以得到总值,则不会处理和计算右边条件。一些非常简单的例子:

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种情况下,你会看到模态窗口要求你做出选择,因为前两种情况取决于右边的部分(即你的选择) ,而后两种情况——尽管这些表达式的总值并不取决于你的选择——因为左边的条件是先读取的。因此,基于您希望首先处理哪些条件,从左到右放置条件非常重要。