同事问了我一个问题:foo.x = foo = {n: 2},foo.x的值是什么?

echosoar 原创发表于 2018/11/27 23:08:51
今天晚上在公司同事突然问了我一个问题:
var foo = {n: 1};
var bar = foo;
foo.x = foo = {n: 2};
最终结果中的foo.x的值是什么? 我定睛一看,这样的问题估计有大坑! 然后琢磨了一下回道:一个对象,其中x层级无限循环:
{
  n: 2,
  x: {
    n: 2,
    x: {...}
  }
}
然而我同事一脸就知道你会这么说的表情告诉我:不对!
瞬间我脑海中闪过千万个年头,作为一个沉浸于碰运气。。。oh不,沉浸于钻研问题多年的小青年,怎么能被小看!
难道是我记错了?这种连等不是从右向左的?
既然这样子,那结果应该是。。。{n: 1} !,瞬间新的答案脱口而出,脑海中浮现出同事诧异且崇拜的表情,,,
“不对!哈哈哈哈”
听到这句话,作为一个程序猿,我的第一反应是“不可能!”,第二反应是“肯定你自己搞错了!”,接下来的反应自然而然就是打开控制台,把代码粘贴进去,加一行console,然后猛敲一下回车!
undefined
麻蛋!为什么是undefined,为什么我竟然会错?这不科学。。。
于是,祭出神器"stackoverflow",我搜搜搜搜
古人诚不欺我,鲁迅大大的名言没错:你遇到的问题在很久很久以前肯定有别人遇到过并且在Stack Overflow上面问了!
Javascript code trick :What's the value of foo.x
不过回答特别简略。。。真的不能理解里面的原理啊,还是我自己来分析一遍吧
为何会如此?且听下回分解
哈哈哈哈!逗你玩的,这就给你一一道来!
对于这个问题首先我们要分析出来在代码层面那一部分是先执行的,可以参考 ES5规范11.13节 进行一些解释:
规范中一上来就说了赋值运算包含哪些形式:
AssignmentExpression :
    ConditionalExpression
    LeftHandSideExpression AssignmentOperator AssignmentExpression

AssignmentOperator : one of
     =  *=  /=  %=  +=  -=  等等等
规范中写了 AssignmentExpression : LeftHandSideExpression = AssignmentExpression 的运算逻辑:
The production AssignmentExpression : LeftHandSideExpression = AssignmentExpression is evaluated as follows:
    1. Let lref be the result of evaluating LeftHandSideExpression.
    2. Let rref be the result of evaluating AssignmentExpression.
    3. Let rval be GetValue(rref).
    4. 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"
    5. Call PutValue(lref, rval).
    6. Return rval.
从上面可以得到的信息是: 1. 计算 LeftHandSideExpression.,然后把结果赋值给 lref 2. 计算 AssignmentExpression ,赋值给 rref 3. 获取rref的值赋给rval 4. 把rval的值赋给lref 5. 返回rval
而 LeftHandSideExpression 左侧表达式我们也可以从 ES5规范11.2节 中找到:
LeftHandSideExpression :
    NewExpression
    CallExpression
有兴趣的童鞋可以翻看一些NewExpression和CallExpression的定义,都在规范的这一节里面,不过其中并不包含=操作,其实我们从上面的AssignmentOperator中也可以得到 LeftHandSideExpression 所代表的就是 foo.x
所以 foo.x = foo = {n: 2}; 可以转换为 foo.x = (foo = {n: 2});
在规范11.2中也定义了LeftHandSideExpression的执行流程:
The production MemberExpression : MemberExpression [ Expression ] is evaluated as follows:
1. Let baseReference be the result of evaluating MemberExpression.
2. Let baseValue be GetValue(baseReference).
3. Let propertyNameReference be the result of evaluating Expression.
4. Let propertyNameValue be GetValue(propertyNameReference).
5. Call CheckObjectCoercible(baseValue).
6. Let propertyNameString be ToString(propertyNameValue).
7. If the syntactic production that is being evaluated is contained in strict mode code, let strict be true, else let strict be false.
8. Return a value of type Reference whose base value is baseValue and whose referenced name is propertyNameString, and whose strict mode flag is strict.
The production CallExpression : CallExpression [ Expression ] is evaluated in exactly the same manner, except that the contained CallExpression is evaluated in step 1.
按照上面的流程,LeftHandSideExpression 其实就是从 MemberExpression (foo)中 获取 baseValue, 从 Expression (x)中获取 propertyNameReference,然后根据他们两个来获取 一个 Reference 类型的值。
而一个 Reference 类型的值按照规范8.7节中说的,包含base value、referenced name 和 strict reference flag 三部分,base value对于foo.x来说就是foo,也就是 {n: 1},reference就是x。
因此lref就是一个 Reference 类型的值,base 是 {n: 1}, reference name是'x'。
现在开始计算 AssignmentExpression 的值,也就是 foo = {n: 2},首先是和上面一样的逻辑,获取 foo 的Reference,即为llref即 base 为 global,reference 为 foo。
然后获取右边的值,也就是{n: 2},即为rrval,赋值给上面的llref,也就是在此处改变了foo的引用变成了 {n:2} ,从此foo与之前的 {n: 1} 没有任何联系了!!!
然后将 rrval 返回给左边的 lref,这个时候lref引用的还是 {n:1} ,所以赋值之后 lref的值就变成了 {n:1, x: {n: 2}} ,不过这个时候已经与foo没有任何关系了,因为引用的联系在上一步已经断掉了。
所以最终foo就是{n:2},而foo.x 就会调用 {n:2}.x ,那么值也就是为 undefined。