`
zhouyrt
  • 浏览: 1163386 次
  • 性别: Icon_minigender_1
  • 来自: 北京
社区版块
存档分类
最新评论

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

阅读更多

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

 

一、引子

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
分享到:
评论
19 楼 flyingzl 2010-10-16  
楼主说的很绕,其实就是一个值引用的问题

var a={}
var b=a;


其实a、b都是指向{}的一个指针,如果a或者b修改,{}这个对象也会修改。。比如a.x=3,那么{}这个对象就修改了。。因为b也指这个对象,所以b也修改了。。。

但是  a.x=a=3

表达式从坐到右,a.x=3首先修改了对象,对象改了,b当然也变化了;但是a=3,a指向了其他的地方,但是并没有修改对象,所以b的值不会改变了。。
18 楼 susaner 2010-10-16  
trains629 写道

第二步时没有重新判断a的指向是否已经改变,直接按之前的指向处理的。

确实,就这一句最有用
17 楼 ghyghoo8 2010-10-16  
神奇!得记下来
16 楼 dojotoolkit 2010-10-16  
a.x = a = {n:2}
解释器首先执行的是a.x,不是赋值。找到a.x是在哪里,就不关心a是啥了。
所以相当于:
({n:1}).x = a = {n:2}
这么就容易理解了。
15 楼 kuchaguangjie 2010-10-16  
这样写有意思吗???
以后谁维护这种代码?
14 楼 sswh 2010-10-16  
RednaxelaFX 写道
嘛,这也是运算符结合性(associativity)与求值顺序(order of evaluation)的概念分不清楚时容易弄错的问题。
结合性和求值顺序是没有必然关系的。

JavaScript的表达式的求值顺序都是从左向右的。赋值运算的结合性虽然是右结合,但同样是从左向右求值的。


这个解释不错。
13 楼 RednaxelaFX 2010-10-16  
嘛,这也是运算符结合性(associativity)与求值顺序(order of evaluation)的概念分不清楚时容易弄错的问题。
结合性和求值顺序是没有必然关系的。

JavaScript的表达式的求值顺序都是从左向右的。赋值运算的结合性虽然是右结合,但同样是从左向右求值的。看ECMAScript 5的附录D,
11.8.2, 11.8.3, 11.8.5: ECMAScript generally uses a left to right evaluation order, however the Edition 3 specification language for the > and <= operators resulted in a partial right to left order. The specification has been corrected for these operators such that it now specifies a full left to right evaluation order. However, this change of order is potentially observable if side-effects occur during the evaluation process.
这里的描述说明ECMAScript是使用从左向右的求值顺序

AssignmentExpression :
    ConditionalExpression
    LeftHandSideExpression AssignmentOperator AssignmentExpression

这条语法规则定义了赋值运算符是右结合的。怎么看出来呢?
首先要能读懂ECMAScript规范里语法的记法。在冒号左边的是语法规则的名字,右边的是规则的推导内容。推导内容中,在同一行上的属于同一条子规则,在不同行上的属于不同子规则;不同子规则之间是“或”的关系。
上面的语法规则的意思是:
一个“赋值表达式”,
  可以由一个“条件表达式”构成;
  或者可以由一个“左手边表达式”加上一个“赋值运算符”加上一个“赋值表达式”构成。

如果在一条语法规则里,推导内容中出现了该规则自身,则这条规则是“直接递归”的。如果自身出现的位置在推导内容某条子规则的最左边,则为“左递归”,出现在最右边则为“右递归”。运算符结合性也正好在此体现:左递归的规则意味着左结合,右递归的规则意味着右结合。
可以看到,ECMAScript的赋值表达式的语法是右递归的,因而是右结合的。

这两者对程序执行有什么影响呢?前面clue的解答已经对路了。有兴趣看结合性、优先级和求值顺序的关系的例子的请参考我之前的一帖,虚拟机随谈(一):解释器,树遍历解释器,基于栈与基于寄存器,大杂烩,中间讲到抽象语法树的时候有举例。Java与C#同JavaScript一样,是用从左向右的求值顺序,而赋值运算是右结合的,所以那帖的例子也可以用来帮助理解JavaScript的状况。
12 楼 trains629 2010-10-16  
老师说了,连续赋值是危险的,尽量不要用。呵呵

应该还是赋值顺序的问题,这种从左到右形式,但在思考的时候容易先从右面开始。
11 楼 sswh 2010-10-16  
LZ又是猜想、又是证明、又是解惑的,但绕了一大圈也并没有把问题说清楚。

其实,最合理的解释也就是下面的,一句话就足够了。

暴走的酱油瓶 写道
这个现象还真出乎意料之外,不看结果的话肯定以为是猜想2,但是a.x会被赋值。感觉像是js引擎为了效率,在执行
a.x = a = {n:2};
1、a = {n:2};
2、a.x = {n:2};
第二步时没有重新判断a的指向是否已经改变,直接按之前的指向处理的。


再引用Clue的,说明这种赋值方式不只是JavaScript、Java也是如此:
O o = new O("1");  
O t = o;  
o.a = o = new O("2");  
System.out.println(o.id + o.a + t.a); 


10 楼 zhengyutong 2010-10-16  
这个问题确实没有仔细思考过。
9 楼 碧血剑 2010-10-16  
写的不错,再看看。
8 楼 lanicetomas 2010-10-16  
JS不在行,用JAVA来看待这问题。
这分明就是一个JAVA基础问题。
搞清楚对象的引用及赋值顺序,是先右侧往左计算,一切不是很简单??
这就是基础。
7 楼 lifesinger 2010-10-15  
挺有意思,尝试分析了下:

http://lifesinger.org/blog/2010/10/a-x-a/
6 楼 rainsilence 2010-10-15  
看了两遍才明白。实际上最后alert(a.x);的a是{n:2},a.x=a的左边的a是{n:1}。
也就是说最后赋值的那个a,没有地方引用,所以alert(a.x);才会是未定义
5 楼 szcjlssx 2010-10-15  
这个不错,我也一直没注意到这一点
4 楼 clue 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
3 楼 暴走的酱油瓶 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,这为什么要搞个连续赋值呢?
2 楼 zhouyrt 2010-10-15  
soni 写道
不是标题党。恐怕我写20年也不会这么赋值。

soni 所言极是,这里仅拿来讨论。
1 楼 soni 2010-10-15  
不是标题党。恐怕我写20年也不会这么赋值。

相关推荐

    javascript实现连续赋值

    这说明了JavaScript中的赋值运算的右结合性如何影响连续赋值。在编写连续赋值代码时,开发者需要考虑到这一点,以避免因理解错误而导致的bug。 此外,当涉及到对象和属性的赋值时,还应特别注意JavaScript中对象...

    使用HTML5、CSS3和JavaScript实现简易四则运算

    在本项目中,我们将探讨如何使用HTML5、CSS3和JavaScript技术来创建一个简单的四则运算计算器。这个计算器能够执行基本的加法、减法、乘法和除法操作,对于初学者来说是一个很好的实践项目,它能帮助理解前端开发的...

    javascript input自动赋值

    在"javascript input赋值"这个压缩包文件中,可能包含了一些示例代码或者教程,用于演示如何在实际项目中应用这些技术。通过学习和理解这些示例,开发者能够更好地掌握JavaScript中对input元素的操作,提高前端开发...

    js 连续赋值的简单实现

    在JavaScript中,连续赋值是一种常见的操作,它允许开发者将一个值赋给多个变量。通常情况下,连续赋值不会引起混淆,但是当涉及对象和引用时,就可能出现一些不易察觉的陷阱。了解这些陷阱和连续赋值的内部机制对于...

    计算器运算逻辑实现(带括号,求余,乘方,加减乘除),不使用eval函数-前端JavaScript 源码实现-括号匹配与初等运算

    加法(+)、减法(-)、乘法(*)和除法(/)是基础,但为了支持括号和更复杂的运算,我们需要了解优先级和运算顺序。在JavaScript中,括号用于改变运算顺序,表达式`a * (b + c)`会先计算括号内的`b + c`,然后将...

    JavaScript实现算术运算算子演示系统

    此外,JavaScript还支持复合赋值运算符,如`+=`, `-=`等,它们可以简化代码并进行连续运算: ```javascript a += b; // 等价于 a = a + b a -= b; // 等价于 a = a - b ``` 在HTML5中,我们可以利用`&lt;canvas&gt;`元素...

    JavaScript 浮点 运算 函数

    此函数是我自己写的,虽然在网上可以搜到很多,不过我找到的都是在算法中存在基本的浮点数的运算,导致结果仍然是错误的。由于刚刚学写JS,所以可能考虑不够周全,望大家批评指正。 代码中加了四舍五入函数,是网上...

    Javascript做的计算器源代码

    JavaScript是一种广泛应用于网页和网络应用开发的脚本语言,它主要负责处理客户端的交互逻辑,为用户提供...通过这个项目,你可以深入了解浏览器环境下的JavaScript运行机制,以及如何利用JavaScript提高网页的互动性。

    PHP+JavaScript+HTML变量之间赋值及传递1

    在构建Web应用时,PHP、JavaScript和HTML之间的变量赋值和传递是至关重要的。这篇文章主要讲解了如何在这些技术之间进行数据交互。 首先,我们来看HTML超链接如何传递值。在HTML中,我们可以使用`&lt;a&gt;`标签创建链接...

    JAVASCRIPT通过移位操作实现四则运算

    JavaScript是一种广泛应用于Web开发的脚本语言,它在处理四则运算时提供了多种方式,包括基本的算术运算符和位运算符。本篇将详细探讨如何利用位移操作实现四则运算,以及这些方法在实际编程中的应用。 在...

    JavaScript内核系列 pdf

    JavaScript语法基于ECMAScript规范,包括变量声明(var、let、const)、数据类型(如字符串、数字、布尔值、对象、数组等)、运算符(算术、比较、逻辑、位运算等)、流程控制(条件语句、循环结构)以及函数等。...

    李炎恢JavaScript全套课程讲义

    **JavaScript全方位解析** JavaScript,简称JS,是一种广泛应用于网页和网络应用的脚本语言,尤其在前端开发领域占据着核心地位。它与HTML和CSS一起,构成了网页开发的三驾马车,使得网页具备交互性、动态性和实时...

    JavaScript\JavaScript教程

    JavaScript是一种广泛应用于Web开发的脚本语言,由Netscape公司的Brendan Eich在1995年发明。它最初被命名为LiveScript,后来为了与Sun Microsystems的Java语言搭上关系,更名为JavaScript,但实际上两者并无直接...

    【JavaScript源代码】JavaScript中极易出错的操作符运算总结.docx

    本文主要探讨了JavaScript中几个易出错的操作符类别,包括算术运算符、比较运算符、逻辑运算符以及赋值和一元运算符的运算顺序和优先级。 首先,我们来看算术运算符。在JavaScript中,有些特殊的字面量值,如`NaN`...

    javascript float运算

    javascript float运算会有误差,可以用此脚本中的函数进行修补。

    JavaScript中的数学运算介绍.docx

    JavaScript中的数学运算主要涵盖基本算术运算和使用Math对象的高级计算。JavaScript支持常见的加法(+), 减法(-), 乘法(*), 除法(/)以及取模(%)运算符。此外,它还提供了处理特殊计算结果的能力,如正负Infinity和NaN...

    Javascript模拟训练

    10. 表达式运算:Javascript中的表达式运算可以是算术运算、比较运算、逻辑运算等,例如`a++==b?a:b`的结果是根据条件判断的结果而定的。 11. Radio按钮默认选中:在Html中,可以使用checked属性来设置Radio按钮的...

    javascript10源码大全

    JavaScript10源码大全是一个关于JavaScript编程语言的资源集合,主要针对那些想要深入理解JavaScript核心概念、函数以及工作机制的开发者。这个压缩包可能包含了多个示例、教程、代码片段,甚至是完整的项目源码,...

Global site tag (gtag.js) - Google Analytics