Open guyuezhai opened 4 years ago
结果: undefined {n:2}
首先,a和b同时引用了{n:2}对象,接着执行到a.x = a = {n:2}语句,尽管赋值是从右到左的没错,但是.的优先级比=要高,所以这里首先执行a.x,相当于为a(或者b)所指向的{n:1}对象新增了一个属性x,即此时对象将变为{n:1;x:undefined}。之后按正常情况,从右到左进行赋值,此时执行a ={n:2}的时候,a的引用改变,指向了新对象{n:2},而b依然指向的是旧对象。之后执行a.x = {n:2}的时候,并不会重新解析一遍a,而是沿用最初解析a.x时候的a,也即旧对象,故此时旧对象的x的值为{n:2},旧对象为 {n:1;x:{n:2}},它被b引用着。 后面输出a.x的时候,又要解析a了,此时的a是指向新对象的a,而这个新对象是没有x属性的,故访问时输出undefined;而访问b.x的时候,将输出旧对象的x的值,即{n:2}。
上面是之前写的解释,最近看周爱民老师的文章的时候,发觉这部分解释有不少地方没说到本质上,有的还是错误的,所以我重新结合老师的文章研究了一下,修改如下: 以这段代码为例:
var a = {n:1};
a.x = a ={n:2};
console.log(a.x);
代码 | 注释 | 补充 |
---|---|---|
a | 计算单值表达式 a,得到 a 的引用 | 这里的 a 是初始 a |
a.x | 将 x 这个标识符作为. 运算符的右操作数,计算表达式 a.x,得到结果值(Result),它是一个 a.x 的“引用” | 这个“引用”当作一个数据结构,通常有 base、name、strict 三个成员。无论x 属性是否存在(这里暂时不存在),a.x 都会被表达为 {"base": a, "name": "x", ...}。而这里的 a 仍然指向旧对象。 |
a | 计算单值表达式 a,得到 a 的引用 | 这里的 a 是初始 a |
a = {n:2} | 赋值操作使得左操作数 a 作为一个引用被覆盖,同时操作完成后返回右操作数 {n:2} | 这里的这个 a 的的确确被覆盖了,这意味着往后通过 a 访问到的只能是新对象。但是,有一个 a 是不会变的,那就是被 a.x 的 Result 保存下来的引用 a,它作为一个当时既存的、不会再改变的结果,仍然指向旧对象。 |
a.x = {n:2} | 指向旧对象的 a 新建了 x 属性,这个属性关联对象 {n:2} | 注意,这里对 a.x 进行了写操作(赋值),直到这次赋值发生的那一刻,才有了为旧对象动态创建 x 属性这个过程。 |
所以,旧对象(丧失了引用的最初对象)和新对象(往后通过 a
可以访问到的那个对象)分别变成:
// 旧对象
a:{
n:1,
x:{n:2}
}
// 新对象
a:{
n:2
}
现在,执行 console.log(a.x)
,这里 a.x
被作为 rhs(右手端) 读取,引擎会开始检索是否真的有 a["x"]
这个东西,因为此时通过 a
能访问到的只能是新对象,它自然是没有 x
属性的,所以打印 undefined
。而且 —— 直到这次读取发生的那一刻,才有了为新对象动态创建 x
属性这个过程。
Note:也就是说,在引擎从左到右计算表达式的过程中,尽管可能遇见类似 a.x
这样本不存在的属性,但无论如何,都会存在 {"base": a, "name": "x", ...}
这样的数据结构,而在后续真正对 x
进行 读写 的时候,这个 x
才会得到创建。
这个代码块所做的事情,实际上是向旧有对象添加一个指向新对象的属性,并且如果我们想要在后续仍然持有对旧对象的访问,可以在赋值覆盖之前新建一个指向旧对象的变量。
Originally posted by @Chorer in https://github.com/Advanced-Frontend/Daily-Interview-Question/issues/93#issuecomment-482389152