本文已同步更新至前端技术公众号: 想养猫的前端,欢迎伙伴们关注😀
最近在github
上看到了一道很有意思的关于JS的连续赋值的题,感觉细节很多,所以拿出来分享一下
var a = { n:1 };
var b = a;
a.x = a = { n:2 };
console.log(a.x); // undefined
console.log(b); // { n:1, x:{n:2} }
首先,在探索这个问题的时候,我认为需要有以下几个知识储备:
- JavaScript 数据类型与指针指向性
- 赋值运算符与运算顺序
JavaScript数据类型及指针指向
JavaScript的数据类型分为两类,一类是存在值存于栈内存的基本数据类型,一类是值存于堆内存中的引用数据类型
与其他语言不同的是,JavaScript不允许直接对内存进行操作(这里指的是对象赋值的时候,其实对对象属性进行操作的时候还是会对内存进行操作),考的是引用对对象进行赋值操作。
也就是说,我们对变量赋予引用类型的值的时候(比如对象),其实赋予的是这个对象的引用(可以理解为这个对象在内存中的地址)
赋值运算符与运算顺序
JavaScript中的赋值运算符 =
的优先级是除了 ,
以外最低的,并且是从右向左结合的
譬如:
var a = b = 1;
其实可以看作:
var a = (b = 1);
而 JavaScript 中的运算顺序是从左向右的。
什么意思?我们一起带着这点知识储备来看看题吧
过程分析
第一行:
var a = { n:1 };
这一步很好理解,声明了一个变量 a
,并且给这个变量赋予了一个对象,而这个变量在栈内存中存的值是这个对象的引用,这个引用指向的是堆内存中的某个值,这个值为 { n:1}
,为了方便后续的理解,我将这个值所存在的堆内存空间叫做 N
第二行:
var b = a;
这一行也好理解, 创建一个变量b,为其赋值对象a。在栈内存中,a与b是不同的,是两个变量,但是他们的指针是相同的,指向同一个堆,也就是我们的 N
, 因为他们栈内存中存的值是一样的,都是同一个地址
第三行:
a.x = a = { n:2 };
这一行便是整个题目的核心。这一行我们可以看作:
a.x = (a = { n:2 });
还记得刚刚说的 JavaScript 中的运算顺序是从左向右的
吗?
这里我们的 a.x
先进行了运算,它在我们 a
所指的堆内存中创建了一个新的属性 x
, 并且赋予了初始值 undefined
, 也就是 N
中多了一个 x
属性
创建完成后,左侧的引用已经指向了 N中的x
,并且会先挂起,等待右侧的 a = { n:2 }
的运算结果,再进行赋值。
我们来看看右侧的赋值。需要注意的是,右侧的赋值完成后,我们的 a
便不再指向 N
,而是指向 {n:2}
这个对象。
由于我们的变量 b
还存放着 N
的地址 , 所以导致我们的 N
并没有因为 a
更改引用而被销毁。
好了,现在右侧的赋值完成了,我们也将最终的结果赋值给我们 N中的x属性
,也就是赋予 {n:2}
这个对象的引用给我们的 x
其实上面的这行代码我们可以看作:
b.x = undefined;
a = { n:2 };
b.x = a;
接下来我们再来打印下a
和 b
,结果其实就相当于:
a = {
n:2
}
b = {
n:1,
x: {
n:2
}
}
因为直接引用一个对象不存在的成员时,会自动地进行创建并且赋予初始值undefined
,所以此时打印 a.x
便会得到 undefined
我觉得这道题还是挺坑的,也有很大的变化空间。譬如 var b = a;
这个点,因为这个所以才不会使得 a
改变引用的时候将原有的堆内存空间销毁,大家可以试试再在这上面做文章进行一个举一反三的变化。