堆内存和栈内存
比如我们买电脑的时候,一般都会关注电脑的运行内存,那么运行内存究竟是干嘛的,存了什么东西呢。
**当我们浏览器打开一个页面或者说一个程序的时候,首先它会在我们的内存条中分配出两块内存来(每次打开都会分配),分别是堆内存和栈内存,用于程序运行和变量存储。**如果内存比较小,占用过高的时候,电脑就会卡顿,所以在性能优化里面,我们时常要考虑怎么能把一些没用的东西释放掉,这就叫浏览器的垃圾回收机制,这里先不展开讨论。
stack 栈内存 又叫 ECStack 执行环境栈,它的作用如下:
- 供代码执行
- 存放声明的变量
- 存放原始值类型
heap 堆内存,它只有一个作用:
- 存放对象数据类型的值
GO(全局对象)
浏览器给我们提供了很多内置的 api,比如 setTimeout, setInterval 等,这些方法是挂在全局对象上的,所以每次一打开页面,都会在开辟出的堆内存中开辟出一个空间,用来存放这些全局可以调用的属性和方法,而这个处于堆内存中的一小块空间,我们称之为 GO (global object)。
VO (变量对象)
最开始,要执行全局下的代码,会先形成一个全局上下文 EC(G),把它进栈执行。为了让我们在每个执行上下文中声明的变量有地儿存,所以每个执行上下文中(全局的 | 私有的 | 块级上下文)都会形成一个东西,叫 VO(变量对象),全局上下文中的叫 VO(G), 用来存放当前上下文中声明的那些变量。
那为什么我们在全局上下文中,能访问到诸如 setTimeout 之类的 api 呢,原因其实很简单,每次创建 VO 时,浏览器会默认帮我们创建一个 window 全局变量(一个指向GO的指针)到 VO 中。
以上都是浏览器默认帮我们干的事情。
全局对象 GO 和全局变量对象 VO(G) 的区别
乍一看,好像都一样,其实它们两个有着本质的区别。
- GO 是存放在堆内存中的,而 VO(G) 占用的是栈内存。
- GO 中存放的当前浏览器环境提供的内置的属性和方法,而 VO(G) 存放的是我们全局代码执行过程中声明的那些变量(let, const, class)。(全局上下文通过 var / function) 或者 window.xx = xx 赋值的,还是会存放到 GO 所在的堆内存中。
注意:如果非全局上下文中的 var 声明的变量的变量,会放在栈内存哦。
JS 代码执行过程
我们来看一段代码,这段代码是在全局上下文执行的:
var a = 12;
var b = a;
b = 13;
console.log(b);
其实在真正执行到我们代码之前,还有一系列要做的事情,比如针对全局执行上下文的词法分析:变量提升,针对函数执行上下文:初始化作用域链,初始化this, auguments, 形参赋值,变量提升等,块级执行上下文中也有自己的特点,这里先不展开赘述。
假设这时候代码开始执行了,遇到
var a = 12;
// - step1: 创建值
// - 原始值存储到栈中
// - 对象的值需要在堆内存空间中,开辟一块空间来(16进制地址),在空间中存储,对象的键值对,然后把地址放入栈内存中,供变量的引用。
// - 这里情况特殊,全局上下文 var 声明的变量值,会存放到 GO 中。
// - step2: 声明变量 declare
// - 把声明的变量名存储到当前的变量对象当中,let,const 均如此,
// - 但是 ECG 中 var 声明的变量还是会放到 GO 中,先在 VO 中找
// - 到 GO 的16进制地(window),然后在 GO 上面添加 a: 12,VO 中没有存 a 哦
// - step3: 赋值操作 defined
// - 让变量和对应的值关联在一起(指针指向的过程)
然后走到
var b = a;
// - 此时 a 不是一个值,而是一个变量,不用做创建值的操作。
// - step1: 声明变量 declare
// - 在全局上下文中的 VO 中(VOG),访问执行 GO 内存地址的变量名 window,
// - 并把变量名作为key,变量值作为val 插入到 GO 中。
// - step3: 赋值操作 defined
// - 让 b 也执行 a 对应的 16 进制地址
接下来就简单了
b = 13;
// - step1: 创建值
// - 把 13 存储到 GO 开辟的堆内存中
// - 已存在 b 变量,不再重新声明
// - step3: 赋值操作 defined
// - 让 b 和 13 关联在一起
打印结果
console.log(a); // 12
// - 查询当前 VO 中 变量标识符为 a 的值,得到 12,如果没找到会再去 GO 中找,再找不到
// - 就会报错啦。这也是为什么我们能通过 a 来访问 window.a 的原因,它会
// - 先看 当前 VO 中有没有 a 变量标识符,这里是没有,就在 GO 中查找。
结合下图理解:

🤔: 如果操作的是对象类型的值呢
var a = { n: 12 };
var b = a;
b['n'] = 13;
console.log(a.n);
与原始值类型的区别唯一就是,一个按值操作,一个按引用地址操作。而且对象类型值是存储在堆内存中的,并把16进制的内存地址交给栈内存,栈内存中(VO)会创建一个变量来指向这个地址。
结合下图理解:

扩展
全局上下文,我们执行如下代码,变量(变量名)存储在哪里呢:
var a = 13; // GO (堆)
let b = 14; // VOG (栈)
const c = 15; // VOG (栈)
d = 16; // GO (堆) 省略关键字相当于 window.d 声明
// 先在 VO 中找,找到了,就不去 window 找了,反正 window 也没存
console.log(b);
画图总结:

小试牛刀
以下代码会输出什么,为什么?
var a = {
n: 1
}
var b = a;
a.x = a = {
n: 2
}
console.log(a.x);
console.log(b);
这个题比较简单,只有一点需要注意
a.x = a = { n: 2 }
// - 题解: 这一步在堆内存中开辟一个对象堆内存空间,存储 { n: 2 }
// - 假设 GO 地址 0x000, 对象 { n: 1 } 堆地址为 0x001, 对象 { n: 2 } 堆地址 0x002
// - 点操作符优先级为 20,比等号高,所以此时的 a.x = a = 0x002
// - 先执行的是 a.x,a 没有 x,所以 0x001 存储的数据值为 { n: 1,x:undefined }
// - 然后给 a 改变指向到 0x002
// - 然后赋值给 0x001.x = 0x002
// - 所以此时 a -> 0x002 也就是 { n: 2 }
// - b -> 0x001 也就是 { n: 1, x: { n: 2 } }
故答案为
- undefined
- { n: 1, x: { n: 2 } }
图示如下:
