这篇文章上次修改于 336 天前,可能其部分内容已经发生变化,如有疑问可询问作者。
最近在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 改变引用的时候将原有的堆内存空间销毁,大家可以试试再在这上面做文章进行一个举一反三的变化。