从一次连续赋值体验搞懂JavaScript存储与赋值😆

1,636 阅读4分钟

本文已同步更新至前端技术公众号: 想养猫的前端,欢迎伙伴们关注😀

最近在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

J0ZcfU.jpg

第二行:

var b = a;

这一行也好理解, 创建一个变量b,为其赋值对象a。在栈内存中,a与b是不同的,是两个变量,但是他们的指针是相同的,指向同一个堆,也就是我们的 N , 因为他们栈内存中存的值是一样的,都是同一个地址

J0e9tf.jpg

第三行:

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 更改引用而被销毁。

J0eF1g.jpg

好了,现在右侧的赋值完成了,我们也将最终的结果赋值给我们 N中的x属性,也就是赋予 {n:2}这个对象的引用给我们的 x

其实上面的这行代码我们可以看作:

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

接下来我们再来打印下ab,结果其实就相当于:

a = {
	n:2
}

b = {
	n:1,
	x: {
		n:2
	}
}

因为直接引用一个对象不存在的成员时,会自动地进行创建并且赋予初始值undefined,所以此时打印 a.x 便会得到 undefined

我觉得这道题还是挺坑的,也有很大的变化空间。譬如 var b = a; 这个点,因为这个所以才不会使得 a 改变引用的时候将原有的堆内存空间销毁,大家可以试试再在这上面做文章进行一个举一反三的变化。