1

我想快速连续地使用运算符对几个变量做一些事情。我不认为想做的事情本身很重要;我的问题更多是关于 JavaScript 评估的基础知识。

在下面的三个示例中,我尝试使用加法来更改两个变量的值。然而,并不是所有的都像我(也许是天真的)预期的那样表现。

JSFiddle在这里。

  1. 作为三个单独的声明的操作

    var a = 9, b = 2;
    a += b; b += a; a += b;
    // a === 24, b === 13
    
  2. 用逗号分隔的操作

    var a = 9, b = 2;
    a += b, b += a, a += b;
    // AS EXPECTED: a === 24, b === 13
    
  3. 一个语句/表达式中的操作

    var a = 9, b = 2;
    a += (b += (a += b)); 
    // BUT HERE WE GET THIS: a === 22, b === 13
    

在最后一个示例中,b按预期计算,但a计算结果比前两个示例中出现的值少二。

我认为这是因为括号中的所有内容都返回正确的值,但最终添加到 的原始值a,即9,而不是(a += b)前面优先级建议的值11

我已经在 Flanagan 的JavaScript: The Definitive Guide (6th ed.)中寻找了原因,(特别是在 4.11.1 “Assignment with operation”下)但在那里什么也没找到。Crockford 似乎也没有在The Good Parts中明确提及它。我使用了各种搜索词来尝试查找有关此行为的更多信息。谁能告诉我这种现象叫什么或指出有关此行为的一些信息(假设它是预期的)或我可能做错了什么(假设不是)?


注意。我意识到示例 3 中的括号可能是多余的,因为据我所知,赋值优先级无论如何都是从右到左的。但我认为让他们在那里会使这个例子更容易谈论。


更新

从下面的答案来看,我认为我对这个问题的困惑实际上源于从弗拉纳根的书中吸收了几段,可能是错误的:

在大多数情况下,表达式:

a op= b

其中op是一个运算符,等价于表达式:

a = a op b

在第一行,表达式a被计算一次。在第二个中,它被评估两次。仅当 sidea包含诸如函数调用或增量运算符之类的副作用时,这两种情况才有所不同。例如,以下两个赋值是不相同的:

data[i++] *= 2
data[i++] = data[i++] * 2

我认为这意味着我的一行示例应该产生与其他两个相同的结果,因为:

  1. Flanagan 提到了两个评估发生在a = a op b而不是一个,这意味着这实际上与右侧未评估为 an的a op= b地方不同。alval
  2. 我假设我使用的赋值运算符(例如a += b)会算作副作用。

恕我直言,我认为 Flanagan 让这件事变得令人困惑,它似乎与 ECMAScript 约定中的内容相矛盾(由pocka粘贴在下面),但这可能是我的阅读/误解。他说的是不正确还是不清楚?或者,就我一个人吗?

4

2 回答 2

2

根据 ECMA-262 第 5 版。11.13.2,复合赋值运算符的评估如下:

  1. 令 lref 为评估 LeftHandSideExpression 的结果。
  2. 令 lval 为 GetValue(lref)。
  3. 令 rref 为评估 AssignmentExpression 的结果。
  4. 设 rval 为 GetValue(rref)。
  5. 令 r 为将运算符 @ 应用于 lval 和 rval 的结果。
  6. 如果以下条件都为真,则抛出 SyntaxError 异常: Type(lref) is Reference 为真 IsStrictReference(lref) 为真 Type(GetBase(lref)) is Environment Record
    GetReferencedName(lref) 为“eval”或“arguments”
  7. 调用 PutValue(lref, r)。
  8. 返回 r。

最后一个例子的评估如下:

1. Let a be lref, Let (b += (a += b)) be rlef.
2. Evaluate a and Let lval be the result of it(9).
3. Evaluate (b += (a += b)) and Let rval be the result of it.
   a. Let b be lref_, Let (a += b) be rlef_.   
   b. Evaluate b and let lval_ be the result of it(2).
   c. Evaluate (a += b) and let rval_ be the result of it.
     A. Let a be lref__, let b be rlef.
     B. Evaluate a and Let lval__ be the result of it(9).
     C. Evaluate b and Let rval__ be the result of it(2).
     D. Put lval__ + rval__ (means 9+2) to lref__(a) and return it.   
   d. Put lval_ + rval_ (means 2+11) to lref_(b) and return it.
4. Put lval + rval (means 9+13) to lref(a) and return it.

那么我们可以得到a === 22, b === 13.

于 2013-11-03T16:12:08.343 回答
2

我认为(不确定,虽然这是违反直觉的)你可以想象:

a += (b += (a += b));

被写为:

a = a + (b += (a += b));

尽管加号+运算符具有从右到左的关联性,但 JavaScript 表达式是从左到右计算的,因此a首先计算的是9now,然后(b += (a += b))计算为13

现在+运算符从右到左添加,因此添加139并给我们22

编辑:我不会直接评论您的问题,因为阅读它们时我会感到困惑:)。

相反,我将尝试以不同的方式解释这一点。我认为您混淆的主要来源来自运算符优先级、关联性和评估顺序之间的差异。

我真的建议你阅读关于评估顺序的部分(书中的 4.7.7,顺便说一句,这是一本很棒的书)

我们先来看一个例子:

var x =1, y = 2, z = 3;
var alpha = (z=4) + y * z;
console.log(x); // 1
console.log(y); // 2
console.log(z); // 4
console.log(alpha);  // 12

在这个例子中,虽然乘法运算符*的优先级高于求和运算符+,但整个表达式的不同分量的求值仍然是从左到右的。

alpha左边首先被声明和创建,然后(z=4)被评估,然后y被评估为2. 现在z再次计算得到,注意这是新值,它是由表达式中前面4赋值的副作用引起的,记住。4z(z=4)

这导致 的总体值alpha等于12

现在回到我们原来的表达式:

a += (b += (a += b));

a左边的第一个被评估的是9现在,然后b左边的第一个被评估的是现在2,现在第二个a被评估的是9也是,然后b右边的最后一个被评估的是再次2

现在开始真正的工作,因为括号最后(a += b)是评估所以现在我们有a = 11,然后(b += (a += b))评估是现在13现在这个值与已经评估的值相加,结果是922

如果它没有以这种方式发生,这意味着a在左侧=将被评估两次,但事实并非如此。

摘要:您无法更新已评估的表达式的值。

我希望这可以为您解决这个问题,如果您有任何其他问题,请随时提问:)

于 2013-11-03T15:22:49.243 回答