更新(1/23/2018)
写在前面,本文让读者产生了误会。有这样一些原因:
- JS为何能取到地址值。
a.x = a
未解释清楚。- .运算符是否会对赋值运算有所干扰。
首先:本文用addr只是一个代称,表达的是该地址对应的那块内存。 关于2.3点就是本次更新的原因
a.x = a
赋值表达式先确定左值(可以这样理解,如果不确定我要去的地方,取到值又有什么用呢?左边的值在执行赋值之前就已经确定了),然后再将右边表达式的返回值给到左值。那么a.x = a = {n : 2}
就是从左往右先确定a.x
再确定a
,然后将返回值从右往左依次赋值给左边。- .运算符会对赋值运算符有优先级干扰吗?(会因为a.x优先级高于a = {n : 2}而先执行a.x = a吗?)下面我来尝试一下:
将题目改写成:
var a = {n: 1};
var b = a;
a.x = a = a.y = {n: 2};
//改写成这样,那么怎么确定优先级呢?
复制代码
现在按照我文章的思路来:
- 先确定所有左边的地址值(再次强调指的是对应的内存!):
addr(a.x) = addr(a) = addr(a.y) = addr({n : 2})
- 我们假设:
addr(a) = 0x100,
addr(a.x) = 0x101,
addr(a.y) = 0x102,
addr({n : 2}) = 0x888,
addr({n : 1}) = 0x999
复制代码
2.(从右往左)将右边的值赋值给左值,然后将其作为该赋值表达式的值返回:
1. 先执行:addr(a.y) = addr({n : 2}),将{n : 2}的地址值存放在addr(a.y)这个地址值对应的内存!中。
(别用箭头指,容易混淆,简单的当做赋值就好了。本文就是犯得这个错,导致没有说清楚。)
2. 然后将addr(a.y) = addr({n : 2})的右值作为该表达式的返回值N返回,在这里会作为下一个赋值表达式的右值。
3. 接着执行addr(a) = N,同上返回N。
4. 接着执行addr(a.x) = N,返回N。
然后我们执行
console.log(a); // {n : 2}
console.log(a.x); // undefined
console.log(a.y); // undefined
console.log(b.x); // {n : 2}
console.log(b.y); // {n : 2}
复制代码
不对呀,为什么给a.x和a.y的地址值对应的内存赋值了,但却没有东西,你是不是讲错了?
因为此时a(0x100)这块内存存的是对{n : 2}的引用,它会去0x888这块内存中找,这样肯定找不着x和y,因为他们在0x999内存中。
而b在一开始var b = a的时候,就将a(0x100)这块内存中存的(0x999)拷贝过来。而y和x就在0x999对应的内存下。所以b找的到。
希望本次更新对有困惑朋友有所帮助!(也许是我讲复杂了,希望多多提问。)
有这样一道面试题
var a = {n: 1};
var b = a;
a.x = a = {n: 2};
alert(a.x); // undefined
alert(b.x); // [object, Object]
复制代码
一开始没有太好思路,或者说是没有想明白,经过一番折腾,算是整理清楚了思路,接下来会一一讲明白,希望能对其他人有所帮助。
开始之前,需要清楚赋值表达式是怎么执行的。首先先明白什么是什么是右结合性和什么是赋值表达式:
右结合性
赋值运算符是右结合性的,如果不知道,就请记住啦! 形如:
A = B = C = D
等价于
A = (B = (C = D))
赋值表达式
A = B
这就是一个赋值表达式,并且一个赋值表达式存在一个左值和一个右值,这可不是胡编乱造的,咱们说话有理有据: 引用链接(11.13.1 Simple Assignment ( = ) )
The production AssignmentExpression : LeftHandSideExpression = AssignmentExpression is evaluated as follows:
- Let lref be the result of evaluating LeftHandSideExpression.
- Let rref be the result of evaluating AssignmentExpression.
- Let rval be GetValue(rref).
- Throw a SyntaxError exception if the following conditions are all true:
- Type(lref) is Reference is true
- IsStrictReference(lref) is true
- Type(GetBase(lref)) is Environment Record
- GetReferencedName(lref) is either "eval" or "arguments"
- Call PutValue(lref, rval).
- Return rval.
翻译过来:
大体来说 赋值表达式:左边的表达式 = 赋值表达式 具体评判的步骤如下:
- 将比左边的表达式的值称为'lref'
- 将右边赋值表达式的值称为'rref'
- 将'rval' 作为GetValue(rref)'的返回值
- 如果下列情况为true就会报错
- balala~~~~
- 调用 PutValue(lref, rval)
- 返回'rval'
由上面可以清楚的知道表达式运算的流程:
- 先计算左边的表达式(很重要,先计算左边的!这里计算的是左边表达式在内存中的地址值)
- 在计算右边的表达式(先左后右,这里计算的是右边的值,这里的值就是值,并不特指地址!若是对象则为地址值)
- 计算右边的值
(等等,这里有疑问,为什么运算两遍右边?第一遍是计算右边表达式的(+ — * /)所得到的值,第二遍是返回这个结果值,也就是每个表达式都有返回值!)
- 这里balala~~~
- 调用函数PutValue,这个做的才是给值,就是将右边的返回值rval放进左边的lref对应的地址值(也就是对应的内存)
- 然后返回这个表达式的值,也就是rval(这里再次说明表达式有返回值,而且还可以看出这个值是GetValue计算出的右边的值,说白了就是等号右边的值)
如果还没理解的话,没关系,往下看,我会画图帮助理解。
有了以上两个知识点,下面来分析一下题目。
var a = {n: 1};
var b = a;
a.x = a = {n: 2};
alert(a.x); // undefined
alert(b.x); // [object, Object]
复制代码
真正难以理解的是在第三句代码a.x = a = {n: 2}
下面咱们开工吧!
-
第一、二句代码执行之后,内存图如下:
-
第三句代码先进行改写
a.x = a = {n: 2};
//先右结合性
a.x = (a = {n : 2})
//在计算等号左边的值
addr(a.x) = (addr(a) = {n : 2})
//在计算等号右边的值
addr(a.x) = ( addr(a) = value( {n : 2} ) )
复制代码
如图:
如上图来进行运算 addr(a.x) = ( addr(a) = value( {n : 2} ) )
- 先计算所有等号左边的值:addr(a.x)的值为:0x8889、addr(a)的值为:0x0001
(0x8889 <-- (0x0001 <-- (0x9999)))
- 在计算{n : 2}的值为:0x9999(因为{n : 2}是一个对象,所以在这里计算得到的是地址值)
- 将0x9999作为当前{n : 2}的返回值(也就是当前表达式的右边的值)
- 将返回值赋值给0x0001(对应的内存)
- 返回( addr(a) = value( {n : 2} ) )的值:0x9999
- 将返回值赋值给0x8889(对应内存)
- 返回addr(a.x) = ( addr(a) = value( {n : 2} ) )的值: 0x9999
一开始找到当前表达式左右左边的值,也就是他们地址值。然后依次将右值逐个放到对应的内存!
最后因为a的地址是指向0x9999的,且其不存x这个属性,故返回undefined。而b的地址指向里面存在一个保存{n:2}的地址的x属性,故返回的是对象。