论坛首页 Web前端技术论坛

写了10年Javascript未必全了解的连续赋值运算

浏览 32190 次
该帖已经被评为良好帖
作者 正文
   发表时间:2010-10-15   最后修改:2010-10-21

很喜欢蔡蔡 的这个标题,实际蔡蔡已经分析过了,这里借用了。或许有点标题党的意思。看完就知了。

 

一、引子

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

 

这是蔡蔡在看jQuery源码 时发现这种写法的。以上第二句 a.x = a = {n:2} 是一个连续赋值表达式。这个连续赋值表达式在引擎内部究竟发生了什么?是如何解释的?

 

二、猜想

猜想1:从左到右赋值,a.x 先赋值为{n:2},但随后 a 赋值为 {n:2},即  a 被重写了,值为 {n:2},新的 a 没有 x属性,因此为undefined。步骤如下

 

1, a.x = {n:2};
2, a = {n:2};

 

这种解释得出的结果与实际运行结果一致,貌似是对的。注意猜想1中 a.x 被赋值过。

 

猜想2:从右到左赋值,a 先赋值为{n:2},a.x 发现 a 被重写后(之前a是{a:1}),a.x = {n:2} 引擎限制a.x赋值,忽略了。步骤如下:

 

1, a = {n:2};
2, a.x 未被赋值{n:2}

 

等价于 a.x = (a = {n:2}),即执行了第一步,这样也能解释a.x为undefined了。注意猜想2中a.x压根没被赋值过。

 

三、证明

上面两种猜想相信多数人都有,群里讨论呆呆认为是猜想1, 我认为是猜想2。其实都错了。我忽略了引用的关系。如下,加一个变量b,指向a。

var a = {n:1};
var b = a; // 持有a,以回查
a.x = a = {n:2};
alert(a.x);// --> undefined
alert(b.x);// --> [object Object]

 

发现a.x仍然是undefined,神奇的是 b.x 并未被赋值过(比如:b.x={n:2}),却变成了[object Object]。b 是指向 a({n:1})的,只有a.x = {n:2}执行了才说明b是有x属性的。实际执行过程:从右到左,a 先被赋值为{n:2},随后a.x被赋值{n:2}。

 

1, a = {n:2};
2, a.x = {n:2};

等价于


a.x = (a = {n:2});

 

与猜想2的区别在于a.x 被赋值了,猜想2中并未赋值。最重要的区别,第一步 a = {n:2} 的 a 指向的是新的对象{n:2} , 第二步 a.x = {n:2} 中的 a 是 {a:1}。即在这个连等语句

a.x = a = {n:2};

 

a.x 中的a指向的是 {n:1},a 指向的是 {n:2}。如下图

 

 

四:解惑

这篇写完,或许部分人看完还是晕晕的。因为里面的文字描述实在是绕口。最初我在理解这个连等赋值语句时

var a = {n:1};
a.x = a = {n:2};

 

认为引擎会限制a.x的重写(a被重写后),实际却不是这样的。指向的对象已经不同了。引擎也没有限制a.x={n:2}的重写。
谢谢所有参与讨论的人:蔡蔡、猪大肠 、呆呆、雅儒。这个问题最早是蔡蔡提出的。雅儒在 菜鸟灰呀灰 群里每次的讨论都那么投入,认真,哪怕是别人提出的话题。

 

五:结束

呵,以另一个连续赋值题结束。fun执行后,这里的 变量 b 溢出到fun外成为了全局变量。想到了吗?

function fun(){
	var a = b = 5;
}
fun();
alert(typeof a); // --> undefined
alert(typeof b); // --> number
 

 

 

  • 大小: 17.2 KB
   发表时间:2010-10-15  
不是标题党。恐怕我写20年也不会这么赋值。
0 请登录后投票
   发表时间:2010-10-15  
soni 写道
不是标题党。恐怕我写20年也不会这么赋值。

soni 所言极是,这里仅拿来讨论。
0 请登录后投票
   发表时间:2010-10-15   最后修改:2010-10-15
这个现象还真出乎意料之外,不看结果的话肯定以为是猜想2,但是a.x会被赋值。感觉像是js引擎为了效率,在执行
a.x = a = {n:2};
1、a = {n:2};
2、a.x = {n:2};
第二步时没有重新判断a的指向是否已经改变,直接按之前的指向处理的。

ps:随便想请问LZ个问题,这么赋值的写法有何好处?赋值过后,a的指向为{n:2},之前的{n:1}这个具体对象也没有被变量引用了,它的x属性为多少,好像也没有意义了。如果后续要用到a.x还需要重新赋值,否则还是undefined,这为什么要搞个连续赋值呢?
0 请登录后投票
   发表时间:2010-10-15   最后修改:2010-10-15
为了弄明白这个问题,专门去看了ECMAScript(3rd)文档,谈一谈我的理解

1、引用(Reference)与GetValue & PutValue
引用
A Reference  is a reference to a property of an object. A Reference consists of two components, the base object and the property name.

“引用”是引用某个对象的一个属性(可能这个对象并没有这个属性),一个引用含“根对象”与“属性名”两个成员。
后面以“(根对象,属性名)”来表达一个引用

引用
GetValue (V)
1. If Type(V) is not Reference, return V.
2. Call GetBase(V).
3. If Result(2) is null, throw a ReferenceError exception.
4. Call the [[Get]] method of Result(2), passing GetPropertyName(V) for the property name.
5. Return Result(4).

GetValue,即取值操作,返回的是确定的值,而不是引用。(可以理解为变量与变量的值,或指针与指针指向的对象)

引用
PutValue (V, W)
1. If Type(V) is not Reference, throw a ReferenceError exception.
2. Call GetBase(V).
3. If Result(2) is null, go to step 6.
4. Call the [[Put]] method of Result(2), passing GetPropertyName(V) for the property name and W for the value.
5. Return.
6. Call the [[Put]] method for the global object, passing GetPropertyName(V) for the property name and W for the
value.
7. Return.

PutValue操作只对引用生效,在ECMAScript的描述中,修改对象的属性都是通过Refrence + PutValue进行的
(ECMAScript是为了便于表达而引入Reference这个类型,实际上JS语言中并无此类型。The internal Reference type is not a language data type. It is defined by this specification purely for expository
purposes.)

2、成员表达式(MemberExpression)解释过程
引用
The production MemberExpression : MemberExpression [ Expression ] is evaluated as follows:
1. Evaluate MemberExpression.
  53
2. Call GetValue(Result(1)).
3. Evaluate Expression.
4. Call GetValue(Result(3)).
5. Call ToObject(Result(2)).
6. Call ToString(Result(4)).
7. Return a value of type Reference whose base object is Result(5) and whose property name is Result(6).

着重看第7步:a value of type Reference

3、赋值表达式解析
引用
The production AssignmentExpression : LeftHandSideExpression = AssignmentExpression is evaluated as follows:
1. Evaluate LeftHandSideExpression.
2. Evaluate AssignmentExpression.
3. Call GetValue(Result(2)).
4. Call PutValue(Result(1), Result(3)).
5. Return Result(3).

这里可以看到左侧得出的是引用,右侧调用GetValue取得的是确定值。

那么开始分析a.b = a = {n:2}这个表达式,先假设{n:1}这个对象为OBJ1,{n:2}为OBJ2,全局为GLOBAL。

它的解析如下:
a.b = Expression1
Expression1为另一个赋值表达式:
a = {}

首先计算a.b = Expression1,按(3)中赋值表达式运行步骤
step1先得到引用(OBJ1, "b")
step2解析Expression1{
   Expression1解析
   step1得到引用(GLOBAL, "a")
   step2得到一个对象OBJ2
   step3取值,仍是OBJ2
   step4将引用(GLOBAL, "a")赋值为step3结果
   step5返回OBJ2
}
step3取值,结果同样为OBJ2
step4将(OBJ1, "b")赋值为OBJ2
step5返回OBJ2

最终结果:
OBJ1: {n:1, b:OBJ2}
OBJ2: {n:2}
a : OBJ2

PS:
我们常说赋值运算是从右至左,是指右边先结合
所以a.b = a = {n:2}解析为了a.b = ( a = {n:2}),而不会解析为(a.b = a) = {n:2}
如果理解为右边先运算就会有误解了,虽然右边先赋值成功。

另外,测试了Java中同样的案例,发现结果也一样,赋值语句左侧先解析出来。
估计C++中也一样吧……
class O{
	String id;
	O(String id){
		this.id = id;
	}
	O a;
}

O o = new O("1");
O t = o;
o.a = o = new O("2");
System.out.println(o.id + o.a + t.a);

// ---------- console ----------
2nullcom.****$O@16f0472
0 请登录后投票
   发表时间:2010-10-15  
这个不错,我也一直没注意到这一点
0 请登录后投票
   发表时间:2010-10-15  
看了两遍才明白。实际上最后alert(a.x);的a是{n:2},a.x=a的左边的a是{n:1}。
也就是说最后赋值的那个a,没有地方引用,所以alert(a.x);才会是未定义
0 请登录后投票
   发表时间:2010-10-15   最后修改:2010-10-15
挺有意思,尝试分析了下:

http://lifesinger.org/blog/2010/10/a-x-a/
0 请登录后投票
   发表时间:2010-10-16  
JS不在行,用JAVA来看待这问题。
这分明就是一个JAVA基础问题。
搞清楚对象的引用及赋值顺序,是先右侧往左计算,一切不是很简单??
这就是基础。
0 请登录后投票
   发表时间:2010-10-16  
这个问题确实没有仔细思考过。
0 请登录后投票
论坛首页 Web前端技术版

跳转论坛:
Global site tag (gtag.js) - Google Analytics