连续赋值与求值顺序

看到一个2010 年的帖子[1],里面讨论了一段 JavaScript 代码:

1
2
3
var a = {n:1};
a.x = a = {n:2};  
alert(a.x);     // --> undefined

帖子里有很多的讨论来分析为何 a.xundefined,下面是我的一些思考。

首先,在JavaScript中,有以下几点需要明确的:

  1. 可以说一切都是对象,一切都是引用调用。
  2. 赋值运算符(=)是除逗号运算符(,)外优先级最低的,并且是右结合的。[2]
  3. 求值顺序[3]是从左向右的[4]

1
2
3
4
5
6
7
8
9
10
11
a.x = a = {n: 2};
// =>
a.x = (a = {n: 2});
/*
 1. 首先,按 = 号的结合性做个分组;
 2. 然后,由于从左到右的求值顺序和运算符优先级,先计算 a.x,获得指向内存中的某个地址,假定为 A;
 3. 接着计算第一个等号的右边,其为第二个等号的求值结果: 把变量 a 指向对象 {n: 2}, 然后返回该引用;
 4. 然后把该引用赋给 A,即 A 位置存储的值是 {n: 2};
 在第三个步骤计算第二个等号的过程中,把 a 重新指向了另一个对象 {n: 2},而不在是 {n: 1} 了,
 这个时候 a.x 指向的位置已不在是 A 了,故最后得到的 a.x 是 undefined。
*/

不同于一些用于精简代码显示技术的 Trick,连续赋值应该说是较为常用的一种语句,但需要注意的是,尽量少写出这种修改、引用并存的语句。

在 JavaScript 中还好,规定了从左往右求值,但在 C/C++ 中,并没有明确规定求值顺序,在某些情况下尤其是在函数参数中,可能会带来很大的问题。

翻了下邮件,在 2010 年 4 月的时候,向师兄请教了一个简单的问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#include <iostream>
using namespace std;

int add1(int x)
{
    x++;
    return x;
}

int add2(int& a)
{
    a++;
    return a;
}

int main()
{
    int x=0;
    int y=0;
   
    cout << add1(x) << " " << x <<endl;
    cout << add2(y) << " " << y <<endl;
   
    /*
    cout << "调用前: x y"<<endl;
    cout <<"\t"<< x << " " << y <<endl;
    cout << "函数结果" << add1(x) <<" "<< add2(y) << endl;
    cout << "调用后: x y"<<endl;
    cout << "\t"<<x << " " << y <<endl;
    */

   
    system("pause");
    return 0;
}

在 VC 6.0,2008,2010,g++ for windows,还有在 Ubuntu下用 g++ 编译运行都是得到 1 0 1 0 (此处不显示换行)的结果;只有在采用了 g++ 编译器的 DEV C++ 5.0 中得到的是 1 0 1 1

咨询和查资料[5][6][7][8]的结论是:C/C++ 函数参数的求值顺序没有明确的规范,具体依赖于编译器的实现,因此不要写出那种带副作用的连续操作语句。

References

  1. 写了10年Javascript未必全了解的连续赋值运算
  2. Operator Precedence
  3. 运算数的求值顺序
  4. JavaScript的表达式的求值顺序都是从左向右的
  5. 函数的副作用
  6. 浅谈C/C++中运算符的优先级、运算符的结合性以及操作数的求值顺序
  7. Funny thing about C parameter evaluation order
  8. 函数参数的求值顺序

《连续赋值与求值顺序》有2个想法

评论已关闭。