堆栈内存以及 VO(G) 和 GO 的区别

84 阅读6分钟

堆内存和栈内存

比如我们买电脑的时候,一般都会关注电脑的运行内存,那么运行内存究竟是干嘛的,存了什么东西呢。

**当我们浏览器打开一个页面或者说一个程序的时候,首先它会在我们的内存条中分配出两块内存来(每次打开都会分配),分别是堆内存和栈内存,用于程序运行和变量存储。**如果内存比较小,占用过高的时候,电脑就会卡顿,所以在性能优化里面,我们时常要考虑怎么能把一些没用的东西释放掉,这就叫浏览器的垃圾回收机制,这里先不展开讨论。

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) 的区别

乍一看,好像都一样,其实它们两个有着本质的区别。

  1. GO 是存放在堆内存中的,而 VO(G) 占用的是栈内存。
  2. 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 } }

图示如下: