为什么JavaScript中的逻辑运算符是左结合的?

25

逻辑与和逻辑或运算符以及三元条件运算符是JavaScript中唯一的惰性运算符。它们使用以下规则进行短路求值测试:

false && anything === false
true || anything === true

这是Haskell中实现的相同方式:
(&&) :: Bool -> Bool -> Bool
False && _ = False
True  && x = x

(||) :: Bool -> Bool -> Bool
True  || _ = True
False || x = x

然而,根据 MDN,在 JavaScript 中逻辑运算符是左结合的。这很不直观。在我看来,它们应该是右结合的。Haskell 做得很对。Haskell 中的逻辑运算符是右结合的:

infixr 3 &&
infixr 2 ||

考虑以下Haskell表达式:
False && True && True && True

由于 Haskell 中 && 是右结合的,因此上述表达式等同于:
False && (True && (True && True))

因此,表达式 (True && (True && True)) 的计算结果并不重要。由于第一个 False,整个表达式在一步中就被简化为 False
现在考虑如果 && 是左结合的会发生什么。该表达式将等价于:
((False && True) && True) && True

现在需要进行3次简化才能评估整个表达式:
((False && True) && True) && True
(False && True) && True
False && True
False

正如您所看到的,逻辑运算符更适合右结合。这就引出了我的实际问题:

为什么JavaScript中的逻辑运算符是左结合的?ECMAScript规范对此有何说明?JavaScript中的逻辑运算符实际上是右结合的吗?MDN文档关于逻辑运算符结合性的信息是否不正确?


编辑:根据规范,逻辑运算符是左结合的:

LogicalANDExpression = BitwiseORExpression
                     | LogicalANDExpression && BitwiseORExpression

LogicalORExpression = LogicalANDExpression
                    | LogicalORExpression || LogicalANDExpression

4
结果,无论哪种方式,包括短路行为在内,都是相同的,对吗? - Barmar
3
如果结合性不会对结果产生影响,编译器可以采用任意方式进行。规范的作者可能并没有关注此事,因为这并不重要。 - Barmar
1
从您提供的 MDN 表格来看,他们似乎只是让所有二元运算符左结合,除了赋值运算符。在大多数情况下,选择是任意的,因为操作是可交换的。 - Barmar
1
我想我的意思是它们是关联的 可交换的。忽略短路,a && b 等同于 b && a。大多数可交换运算符也是关联的,但我发现了这个链接:http://unspecified.wordpress.com/2008/12/28/commutative-but-not-associative/。 - Barmar
4
不会。由于逻辑与和逻辑或的定义方式,第一个操作数总是被评估,只有当第一次评估不短路时才会评估第二个操作数。你不能在评估第一个操作数之前评估第二个操作数。因此,即使最后一个操作数为“False”,仍需要评估第一个操作数,进而需要评估它的第一个操作数等等。 - Aadit M Shah
显示剩余24条评论
3个回答

15

对于任何一个好的编译器来说,这些运算符的选择结合性基本上是无关紧要的,输出的代码将是相同的。是的,语法树是不同的,但是发出的代码不需要改变。

在我所知道的所有C家族的语言中(其中包括Javascript),逻辑运算符都是左结合的。因此,真正的问题变成了,为什么类C语言将逻辑运算符定义为左结合的?既然选择的结合性无关紧要(从语义和效率方面来说),我的怀疑是选择了最“自然”的结合性(就像“大多数其他运算符使用的”那样),尽管我没有任何来源来支持我的观点。另一个可能的解释是,使用LALR解析器解析左结合的运算符需要更少的堆栈空间(现在这不是一个大问题,但当C出现时可能是)。


1
C语言类似的布尔表达式的求值行为模糊地类似于foldl',这很好,因为它们总是严格的。 - misterbee

5
考虑以下代码:

考虑以下代码:

console.log(   true || (false && true) );   // right associative (implicit)
console.log(  (true || false) && true  );   // left associative (Javascript)

这两个例子实际上返回相同的结果,但这不是担心运算符结合性的原因。在这里相关的原因是由于逻辑运算符确定其结果的独特方式。尽管所有排列最终都会得出相同的结论,但计算的顺序发生了变化,这可能对您的代码产生重大影响。

因此,请考虑以下内容:

    var name = "";

    // ----------------------------------------
    // Right associative
    // ----------------------------------------
    name = "albert einstein";
    console.log("RIGHT : %s : %s", true || (false && updateName()), name);

    // ----------------------------------------
    // Left Associative
    // ----------------------------------------
    name = "albert einstein";
    console.log("LEFT  : %s : %s", (true || false) && updateName(), name);

    function updateName() {
        name = "charles manson";
        return true;
    }

输出结果如下:
    RIGHT : true : albert einstein
    LEFT  : true : charles manson

两种表达式都返回true,但是只有左关联版本需要调用updateName()才能返回答案。右关联版本不同,它仅在(false && updateName())中评估第一个参数,因为第二个参数不能将false变成true。
请记住以下两点:
- 运算符优先级描述了不同运算符类型的复合表达式的嵌套顺序。 - 运算符结合性描述了具有相同运算符优先级的复合表达式的嵌套顺序。
请注意,以上两点都不会改变单个表达式的解释方式,只会改变复合表达式的解释方式。结合性发生在更高的水平上。
运算符的结合性和优先级对语言的行为产生巨大影响。理解这些差异对于能够使用和快速掌握各种编程语言的功能差异至关重要。我对你的问题给予肯定,并希望这能稍微澄清一些事情。保重。

1
“JavaScript中的逻辑运算符是右结合的”这句话是什么意思?规范中的语法将逻辑运算符定义为左结合。解释器示例代码展示了短路求值,但并未显示结合性。 - Pedro Rodrigues
1
你说得对。我记错了。我的主要观点是要解决大家关注表达结果而非影响的问题。谢谢。帖子已更新。 - drankin2112
这可能就是JavaScript将||和&&定义为不同的运算符“类型”的原因,以便它们可以通过优先级来解析,而不是通过结合性。 - Aleksandr Dubinsky
谢谢,这个答案帮助我更好地理解了关联性的本质。 - thewoolleyman
1
这个答案与原问题无关,原问题是关于每个逻辑运算符的左结合性,而不是关于混合使用两者表达式的问题(正如这个答案所解释的那样,这可以通过优先级来解决)。 - Tom

-4
简单的回答:想象一下 var i = null; if (i == null || i.getSomeValue()) ... 当不是左关联时,第二个测试将首先被评估,从而给您一个异常。

1
这不是问题的关键。结合性只在链式多操作符表达式中才有意义,例如 if (i==null || i.isBad() || i.getSomeValue()),它可以被解析为 (i==null || i.isBad()) || i.getSomeValue()(左结合)或 i==null || (i.isBad() || i.getSomeValue())(右结合)。 - leftaroundabout
结合性并不决定哪个操作数先被计算。运算符的定义方式决定了哪个操作数先被计算。请仔细阅读问题。 - Aadit M Shah

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