今天看啥  ›  专栏  ›  Pomelo1213

解析一道JS面试题

Pomelo1213  · 掘金  ·  · 2018-01-22 08:27

更新(1/23/2018)

写在前面,本文让读者产生了误会。有这样一些原因:

  1. JS为何能取到地址值。
  2. a.x = a未解释清楚。
  3. .运算符是否会对赋值运算有所干扰。

首先:本文用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};
//改写成这样,那么怎么确定优先级呢?

现在按照我文章的思路来:

  1. 先确定所有左边的地址值(再次强调指的是对应的内存!):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:

  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"
  1. Call PutValue(lref, rval).
  2. Return rval.

翻译过来:

大体来说 赋值表达式:左边的表达式 = 赋值表达式 具体评判的步骤如下:

  1. 将比左边的表达式的值称为'lref'
  2. 将右边赋值表达式的值称为'rref'
  3. 将'rval' 作为GetValue(rref)'的返回值
  4. 如果下列情况为true就会报错
  • balala~~~~
  1. 调用 PutValue(lref, rval)
  2. 返回'rval'

由上面可以清楚的知道表达式运算的流程:

  1. 先计算左边的表达式(很重要,先计算左边的!这里计算的是左边表达式在内存中的地址值)
  2. 在计算右边的表达式(先左后右,这里计算的是右边的值,这里的值就是值,并不特指地址!若是对象则为地址值)
  3. 计算右边的值

(等等,这里有疑问,为什么运算两遍右边?第一遍是计算右边表达式的(+ — * /)所得到的值,第二遍是返回这个结果值,也就是每个表达式都有返回值!)

  1. 这里balala~~~
  2. 调用函数PutValue,这个做的才是给值,就是将右边的返回值rval放进左边的lref对应的地址值(也就是对应的内存)
  3. 然后返回这个表达式的值,也就是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} ) )

  1. 先计算所有等号左边的值:addr(a.x)的值为:0x8889、addr(a)的值为:0x0001 (0x8889 <-- (0x0001 <-- (0x9999)))
  2. 在计算{n : 2}的值为:0x9999(因为{n : 2}是一个对象,所以在这里计算得到的是地址值)
  3. 将0x9999作为当前{n : 2}的返回值(也就是当前表达式的右边的值)
  4. 将返回值赋值给0x0001(对应的内存)
  5. 返回( addr(a) = value( {n : 2} ) )的值:0x9999
  6. 将返回值赋值给0x8889(对应内存)
  7. 返回addr(a.x) = ( addr(a) = value( {n : 2} ) )的值: 0x9999

一开始找到当前表达式左右左边的值,也就是他们地址值。然后依次将右值逐个放到对应的内存!

最后因为a的地址是指向0x9999的,且其不存x这个属性,故返回undefined。而b的地址指向里面存在一个保存{n:2}的地址的x属性,故返回的是对象。




原文地址:访问原文地址
快照地址: 访问文章快照