一道让我开始怀疑自己的 JavaScript 面试题

开门见山,标题中提到的面试题:

let obj = { num1: 111 };

obj.child = obj = { num2: 222 };

console.log(obj.child); 

你会给出什么答案呢?

不知道你是不是也是这样,反正作看这道题时,心里想的是这也太简单了。只一眼,就得出了答案,{ num2: 222 }。然后,当看到给出的正确答案时,我震惊了。我本以为自己的 JS 基础还是可以的,没想到会栽在这样一道看上去平平无奇的题上。而且,即使在知道答案之后,我还是想不明白这是为什么。后来经过查找资料,我终于想明白了。于是,我将解答这个问题的过程记录在本文。如果你只想知道问题的答案,请直接跳到最后一段。

很显然,这道题考察的重点在于赋值表达式。首先,我们知道赋值运算符 = 是右结合(Right-associative),因此 obj.child = obj = { num2: 222 } 等价于 obj.child = (obj = { num2: 222 })。接下来,我们需要理清楚赋值操作到底是怎样被执行的。而关于 JavaScript 的最权威的信息来源就是 ECMAScript 规范。因此我们在 ECMAScript 规范中找到赋值表达式的运行时语义(Runtime Semantic)章节:13.15.2 Runtime Semantics: Evaluation[1]。题中的表达式符合 AssignmentExpression : LeftHandSideExpression = AssignmentExpression 这条产生规则。其中, LeftHandSideExpression 最终产生 obj.childAssignmentExpression 最终产生括号中的内容 obj = { num2: 222 }。记住这一点后,我们就来看赋值运算具体是如何执行的。

首先,obj.child 既不是对象字面量也不是数组字面量,而且 AssignmentExpression 也不是一个匿名函数定义,因此上述步骤可以大致简化为:

  1. LeftHandSideExpression 的求值(Evaluation)结果记为 lref

  2. AssignmentExpression 的求值结果记为 rref

  3. GetValue(rref) 的返回值结果记为 rval

  4. 执行 PutValue(lref, rval)

  5. 返回 rval

所以我们需要首先得到最终产生 obj.childLeftHandSideExpression 的求值结果。经过在规范中的搜寻,我们可以得知 LeftHandSideExpression 可以产生 MemberExpression。规范中又存在规则 MemberExpression : MemberExpression . IdentifierName。而通过这个规则得到的结果和我们最终需要的 obj.child 已经非常形似了。继续重复这个过程,我们可以确认 obj.child 是可以由 MemberExpression 产生的(过程省略),而且就是用到了上面这条规则。于是,查看这条规则对应的运行时语义:

于是,我们又需要最终产生的是 objMemberExpression 的求值结果。这里直接给出这个结果,是一个可以表示为 { [[Base]]: env, [[ReferenceName]]: obj, [[Strict]]: false, EMPTY } 的 Reference Record。这里的 env 指的是当前执行上下文(running execution context)的 LexicalEnvironment,是一个 Environment Record。这个求值结果被赋给 baseReference,执行 GetValue(baseReference),得到 obj 指向的对象(此时的值可以表示为 { num1: 111 }),我们记为 o1。最终,这个表达式返回一个可以表示为 { [[Base]]: o1, [[ReferenceName]]: 'child', [[Strict]]: false, [[ThisValue]]: EMPTY } 的 Reference Record。

回到 LeftHandSideExpression 那条产生规则对应的运行时语义。我们已经得到 lref, 接下来需要对最终产生 obj = { num2: 222 }AssgimentExpression 进行求值,并赋给 rref。于是又要执行一次 AssignmentExpression : LeftHandSideExpression = AssignmentExpression 这条规则对应的语义。只不过这次的 LeftHandSideExpression 将产生 obj,而 AssignmentExpression 将产生 { num2: 222 }。由于篇幅原因,这里直接给出这一次的 lref,可以表示为 { [[Base]]: env, [[ReferenceName]]: obj, [[Strict]]: false, [[ThisValue]]: EMPTY } 以及 rval,也就是根据字面量 { num2: 222 } 生成的对象,同时记为 o2。然后就到了真正的赋值那一步 PutValue(lref, rval), 其中包含执行等价于 env.SetMutableBinding(obj, o2, false)这一操作的步骤。于是,obj 就指向了 o2。最后,rval,也就是 o2,被返回。

回到上一层的赋值表达式的执行步骤中,我们得到 rref 就是 o2GetValue(rref),即 GetValue(o2),直接返回 o2, 同时又被赋给 rval。于是执行 PutValue(lref, rval)。注意,此时的 lref{ [[Base]]: o1, [[ReferenceName]]: 'child', [[Strict]]: false, [[ThisValue]]: EMPTY }。其中包含执行等价于 o1.[[Set]]('child', o2, o1) 这一操作的步骤。于是,o1 变成了 { num1: 111, child: o2 }

上文省略了 GetValue / PutValue 等抽象操作(abstract operation)以及 [[Set]] 等对象的内部方法(internal method)的具体步骤,因为其中大部分是与本文无关的。但是他们其实还挺有趣的,可以解释很多 JavaScript 的特性,其中也涉及到了原型链。以后有机会可能会另外写一篇文章详细展开聊聊。

至此,我们终于可以给出这个面试题的正确答案了。由于此时的 obj 指向的是 o2,也就是 { num2: 222 },因此 console.log(obj.child) 输出 undefined。而由于已经没有了指向 o1 的引用,我们拿不到 o1 的值。假如,修改题目为:

let obj = { num1: 111 }, ref = obj;

obj.child = obj = { num2: 222 };

console.log(obj.child); 
console.log(ref); 

这样,输出的结果证实了我们的结论是正确的。

  • 17
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Web面试那些事儿

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值