【问题标题】:Why is `(foo) = "bar"` legal in JavaScript?为什么 `(foo) = "bar"` 在 JavaScript 中是合法的?
【发布时间】:2017-05-14 22:42:30
【问题描述】:

在 Node.js 的 REPL(也在 SpiderMonkey 中测试过)序列

var foo = null;
(foo) = "bar";

有效,foo 随后等于 "bar",而不是 null

这似乎违反直觉,因为人们会认为括号至少会取消引用 bar 并抛出 Invalid left-hand side in assignment`

可以理解,当你做任何有趣的事情时,它确实会以上述方式失败。

(foo, bar) = 4
(true ? bar : foo) = 4

根据ECMA-262 on LeftHandExpressions(据我所知)没有有效的非终结符会导致括号被接受。

有什么我没看到的吗?

【问题讨论】:

  • 我会猜到destructuring assignment,但它只有方括号和大括号
  • 猜括号只是强制里面的计算,(foo) === foo,我们也有(true ? foo : bar) === foo;(都正确)。
  • @wostex "这里是一个分组操作符" --- 如果是的话,那就是语法错误。
  • @TERMtm 不知道我是否真的走在正确的轨道上,但根据规范(您链接的那个),12.2.1.5 的第二个词法分析器规则定义了一个CoverParenthesizedExpressionAndArrowParameterList 符合 IsValidSimpleAssignmentTarget,产生其内部 expr
  • (foo) 在这种情况下不是分组运算符,就像方法调用 (obj.foo)()obj.foo 周围没有分组运算符一样。相比之下,(0, obj.foo)() 确实包含分组运算符。前面的调用将obj 作为this 上下文传递给foo(就像obj.foo()),后面的调用没有。见Why comma operator changes this in function call

标签: javascript ecma


【解决方案1】:

确实有效。您可以将任何简单的赋值目标括在括号中。

= 操作的左侧部分是您正确识别的LeftHandSideExpression。这可以通过各种优先级别(NewExpressionMemberExpression)追踪到PrimaryExpression,而这又可能是Cover­Parenthesized­Expression­And­Arrow­Parameter­List

( 表达式[In, ?Yield])

(实际上,当用目标PrimaryExpression解析时,它是一个ParenthesizedExpression)。

所以它至少在语法上是有效的。是否真正有效的 JS 语法由另一个因素决定:早期错误静态语义。这些基本上是散文或算法规则,在某些情况下使某些生产扩展无效(语法错误)。例如,这允许作者重用数组和对象初始化语法进行解构,但只应用某些规则。在early errors for assignment expressions我们发现

如果 LeftHandSideExpression 既不是 ObjectLiteral 也不是 ArrayLiteralIsValidSimpleAssignmentTarget,则它是早期的 Reference Error LeftHandSideExpressionfalse

我们还可以在 evaluation of assignment expressions 中看到这种区别,其中简单的赋值目标被评估为可以赋值的引用,而不是获取解构模式的东西,如对象和数组字面量。

那么IsValidSimpleAssignmentTargetLeftHandSideExpressions 做了什么?基本上,它允许对属性访问进行赋值,并且不允许对调用表达式进行赋值。它没有说明任何关于具有 own IsValidSimpleAssignmentTarget rule 的普通 PrimaryExpressions 的内容。它所做的只是通过Covered­Parenthesized­Expression operation 提取括号之间的Expression,然后再次检查IsValidSimpleAssignmentTarget。简而言之:(…) = … 有效,… = … 有效。它只会为Identifiers(如您的示例中)和属性产生true

【讨论】:

  • 为什么他们会允许这样的语法。是否有任何特定的用例可以帮助而不是混淆?
  • @JIXIang 我不知道——尤其是因为它不允许在所有模式中使用(例如,在有用的地方进行对象解构)。也许只是在定义箭头函数参数覆盖语法时发生的一个失误。你应该在 es-discuss 询问。
【解决方案2】:

根据@dlatikay's suggestion,根据现有的预感,对CoveredParenthesizedExpression 的研究可以更好地了解这里发生的事情。

显然,在spec 中找不到非终端的原因,解释为什么(foo) 可以作为LeftHandExpression 被接受,非常简单。我假设您了解解析器的工作原理,并且它们在两个不同的阶段运行:LexingParsing

我从这个小小的研究切线中了解到,结构 (foo) 在技术上并没有像您想象的那样被传递给解析器,因此也没有传递给引擎。

Consider the following

var foo = (((bar)));

众所周知,这样的事情是完全合法的。为什么?好吧,您可以在视觉上忽略括号,而该语句仍然很有意义。

同样,这是另一个有效的示例,即使从人类可读性的角度来看也是如此,因为括号仅说明 PEMDAS 已经隐含的内容。

(3 + ((4 * 5) / 2)) === 3 + 4 * 5 / 2
>> true

只要了解解析器的工作原理,就可以从中大致得出一个关键观察结果。 (请记住,Javascript 仍在被解析 (read: compiled) 并 然后 运行)所以在直接的意义上,这些括号是 “陈述显而易见的”

说了这么多,到底发生了什么?

基本上,括号(函数参数除外)被折叠成正确的包含符号的分组。 IANAL 但是,用外行的话来说,这意味着括号仅被解释为指导解析器如何对它读取的内容进行分组。如果括号的上下文已经“有序”,因此不需要对发出的AST 进行任何调整,则发出(机器)代码,就好像这些括号根本不存在一样。

假设括号是无礼的,解析器或多或少是懒惰的。 (在这种边缘情况下,这是不正确的)

好的,具体发生在哪里?

根据规范中的12.2.1.5 Static Semantics: IsValidSimpleAssignmentTarget

PrimaryExpression: (CoverParenthesizedExpressionAndArrowParameterList)

  1. 设 expr 为 CoverParenthesizedExpressionAndArrowParameterList 的 CoveredParenthesizedExpression。
  2. 返回表达式的 IsValidSimpleAssignmentTarget。

I.E.如果期望 primaryExpression 返回括号内的任何内容并使用它。

因此,在这种情况下,它不会将(foo) 转换为CoveredParenthesizedExpression{ inner: "foo" },而是将其简单地转换为foo,这保留了它是Identifier 的事实,因此在语法上,而不一定在词法上,有效。

TL;DR

It's wat.

想要了解更多?

查看@Bergi's answer

【讨论】:

  • 谢谢@dlatikay!你帮我找到我需要的东西,以证实我的疑虑。
  • 仔细观察,CoveredParenthesizedExpression 确实明确声明此产品将返回在其内部找到的“词法标记流”。了解这意味着什么,就解释了给定的行为。根据您的评论调整了链接。
  • 括号是解析器标记,而不仅仅是词素,它们不用于“强制词法分析器......如何对它读取的内容进行分组”。解析器完成所有这些。您提到的产生式通过括号达到这一事实证明了这一点。
  • @zerkms 这就是为什么这个覆盖语法的东西如此令人困惑的原因:-/ 它确实是语法中的 ObjectLiteral (虽然不是“对象初始化器”) - 他们甚至note it:“ObjectLiteral 产生式也用作 ObjectAssignmentPattern 的覆盖文法”。在the evaluation 中,他们将其重新解析为AssignmentPattern。不知道为什么这么复杂。
  • 这个答案听起来是错误的——“括号仅被解释为强制词法分析器如何对它读取的内容进行分组”是不正确的。那(将事物“分组”到正确的 AST 中)是解析器的工作,而不是词法分析器。
猜你喜欢
  • 2020-10-03
  • 1970-01-01
  • 2018-06-03
  • 2017-06-23
  • 1970-01-01
  • 1970-01-01
  • 2011-12-27
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多