JS-一个老经典的题目:a.x = a = { n: 2 }

170 阅读3分钟

1. var a

这是一个语句,其中a是标识符,变量名,并不是表达式

不是表达式就意味着不会对a进行求值操作

既然不会求值,自然也就没有了赋值的说法。即,在对a初始化的时候,并不像赋值操作一样。而是给a绑定上一个初始值。

所以a只是一个表达名字的,静态语法分析期作为标识符来理解的字面文本,而不是一个表达式

而对于x = y = 100,x 和 y都是表达式。在对其赋值的时候,会对x和y进行求值操作

  • var a不会返回值
  • var a.x = 100,a不是表达式,所以写法错误

2. a.x = a = { n: 2 }

你知道下面代码的结果吗

  var a = { n: 1};
  a.x = a = { n: 2};
  
  console.log(a.x);

答案是:undefined

  1. JavaScript总是严格按照从左到右的顺序来计算表达式
  2. 所以JS会对a.x先求值,再对a求值,再对{ n: 2}求值。(假设a指向对象A
  3. 大致过程是,再上面的变量挨个求完值之后,拿到了对应的Result,再将{ n: 2 } 的值赋给 a,再将 a = { n: 2 } 的值赋给a.x。 这个时候变量a指向了一个新的对象(假设是B),而a.x不会对B产生任何影响。

为什么?

与其说是a = { n: 2 } 的值赋给a.x,更准确点是将a = { n: 2 } 的值赋给第一步对a.x求值得到的Result,这个Result指向a.x的引用,假设原先a指向对象A,那么这个Result里面存的是A.x的引用。也就是说是将a = { n: 2 } 的值赋给A.x

在整个表达式结束后,A的结构是这样的

{
  x: { n: 2}
}

B的结构实现这样的

{
  n: 2
}

如果没有其他的变量指向A,那A会马上被GC给回收掉,你永远也看不到了。 不过我们将代码改一改就能看见了

var a = {}, b = a;

a.x = a = {n : 2};

console.log(a);		// { n: 2 }
console.log(b);		// { x: { n: 2} }

还有变量b指着呢😁

在对a.x求值之后,拿到了a.x的引用,还拿到了a的引用作为运算结果,存在了一个只有JS引擎才知道的位置。之所以还保留了a的引用,以备在后续的操作中可能会作为this来使用。

假设x是一个方法,调用x的时候,x内部的this自然是a对象了


解释:

对于Result的使用方式,与表达式的位置有关,但求的Result这个过程都是一样的

引用是一个数据结构,用来在引擎层面储存计算过程的中间信息,以及在连续计算中传递这些信息。与C语言指针不同的是,指针里面只有地址的值。

3. 临时性死区

console.log( a );

let a = 1;  //ReferenceError: Cannot access 'a' before initialization

上面的代码会报错,是因为不能在变量初始化之前访问变量。这个行为会被JS引擎识别为uninitial mutable / immutable binding(未被初始化的绑定)

而var声明能够提前使用,是因为var变量总是被引擎初始化为undefined;

var / 函数的声明,是作为varName登记,是变量作用域管理,而其他的声明,作为lexicaName登记,使用词法作用域管理

总结

  1. 声明语句和表达式语句是非常不同的
  2. 理解JS引擎对表达式求值得到的Result,在表达式求值和赋值过程的作用
  3. 临时性死区算是一个补充的小知识点