collins
2022/04/06阅读:22主题:自定义主题1
前端基石:Stack、Heap
前言
在前端来说,JS 代码可运行的环境包括「浏览器环境」、「App 环境(基于 webview)」、「Node 环境」等,但是无论是什么环境下执行 JS 代码,都需要开辟出相关的内存,用来存储值「Heap 堆存储」以及运行代码「Stack 栈内存 -> ECStack 执行环境栈-> Execution Context Stack 执行环境栈」。
GO
我们在 JS 代码运行在浏览器中,浏览器为我们提供了很多内置的API、方法。这些内置的 API 和方法都存在堆内存空间中。当我们打开一个页面时,首先浏览器会在内存在开辟一块空间,来存放内置的 API 和方法。例如 GO(global object)全局对象:存储浏览器内置的 API,并且会为它分配一个16进制的内存地址。

EC(G)
当我们开始执行一段 JS 代码时,会先执行全局的代码。在代码执行时会区分全局的执行环境和私有函数执行环境。为了区分开全局执行环境和私有函数执行环境,每一次函数的执行都会创建一个属于自己的私有执行环境。
全局的执行环境叫做全局执行上下文,又叫做 EC(G) ,它的作用是供全局代码执行。并且提供了全局变量对象 VO(G),用来存储全局下声明的变量对象。

GO VS EC(G)
这里需要区分开全局的变量对象 VO(G) 和全局对象 GO,这是两个不同的东西,但是又是有联系的。
-
GO:是在堆内存中开辟的内存,用来存储全局内置的 API、方法。 -
VO(G):是在栈中开辟的内存,用来存储全局上下文中声明的变量。 但是在浏览器环境中,会默认在 EC(G) 中声明一个变量 window (不同执行环境不一样)来执行堆内存的全局对象。

栈内存 VS 堆内存
栈内存的作用
-
代码的执行环境,将不同地方的代码放置在不同的执行上下文中执行。 -
存储原始值类型的值。 -
提供的变量对象(VO/GO)存储当前上下文中声明的变量。
堆内存的作用
-
存储对象的值,只要是引用类型,就会在 Heap 中开辟空间(16进制地址)来存储对象的键值对(或者函数的代码字符串)。
举个例子
当有如下代码,在浏览器环境中执行。当「声明一个变量等于一个值时」。浏览器会做哪些事情了。
// 全局执行上下文
let a = 1;
var b = 2;
let c = {
name: 'stone',
age: 13
};
当 let 变量 = 值; 时,浏览器会进行三步操作:
-
创建值(原始类型直接存储在栈中,对象类型存储在堆中)。 -
声明变量,在变量对象中声明一个变量。 -
关联变量和值,这个操作称之为定义(赋值)defined。
当 var 变量 = 值; 时和 let 有一点区别:
在「全局上下文」中,基于 let/const 声明的变量,是存储在 VO(G) 中的,但是基于 var/function 声明的变量,是直接存储在 GO 中的,所以严格意义上来讲,基于 var/function 声明的变量是不能称为全局变量的,仅仅是全局对象上的一个属性而已。
var a = 1;
function b() {};
let c = 2;
console.log(window.a);
console.log(window.b);
console.log(window.c);
VM58:4 1
VM58:5 ƒ b() {}
VM58:6 undefined
所以针对上述代码,JS 代码执行会有如下操作。
-
首先在栈内存,VO(G)中创建值 1 并关联变量 a; -
然后在堆内存,GO 中创建值 2 并关联变量 b; -
再然后在堆内存,开辟一块新的内存空间,假设内存地址是 0x 001,存储 「name: 'stone', age: 13」; -
接着在栈内存,声明变量 c ; -
最后将变量 c 和内存 0x 001 关联起来。

「全局上下文」变量的访问和赋值
「全局上下文」访问变量
-
首先查看 VO(G) 中是否存在变量,如果有就是全局变量。 -
VO(G) 没有,在基于 window 查看 GO 有没有,有则是全局对象的一个属性。 -
如果 GO 中也没有,就会报错“xxx is not defined”
「全局上下文」赋值变量:a = 100
-
首先查看赋值变量是否是全局变量,是就修改属性值。 -
如果没有就直接给 GO 加上一个属性值。
面试练习题
接下来看一个面试练习题,我们通过画图的方式来走代码流程
let a = { n:1 };
let b = a;
a.x = a = { n: 2 };
console.log(a.x);
console.log(b);
let a = { n:1 };
当 let 变量 = 值; 时,浏览器会进行三步操作:
-
创建值(原始类型直接存储在栈中,对象类型存储在堆中),所以在堆内存中开辟一块新的内存空间,假设内存地址是 0x001,用来存储 n: 1。 -
声明变量,在变量对象中声明一个变量 a 。 -
关联变量和值,将 a 和内存地址 0x001 关联起来。

let b = a;
-
创建值(原始类型直接存储在栈中,对象类型存储在堆中),发现值就是 a 的值,内存空间地址就是 0x001。 -
声明变量,在变量对象中声明一个变量 b 。 -
关联变量和值,将 b 和内存地址 0x001 关联起来。

a.x = a = { n: 2 };
-
创建值(原始类型直接存储在栈中,对象类型存储在堆中),所以在堆内存中开辟一块新的内存空间,假设内存地址是 0x002,用来存储 n: 2。 -
这里需要注意一下代码的赋值执行顺序。正常情况下当 「x = y = 10;」时,代码是从右往左执行 y = 10; x = 10(或者 x = y),但是这里需要考虑一下优先级的问题,属性访问的优先级高于赋值,所以这里的执行顺序有所不同,a.x = { n:2 },然后才是 a = { n:2 }。3. 在堆内存 0x001 中添加属性 x,并赋值内存 0x002的内存地址。 -
将 变量 a 赋值给新的内存地址 0x002;

画完堆栈内存的关系图,对于输出的结果就很明显了。
console.log(a.x); // undefined
console.log(b); // { n:1 x: { n: 2 } }
你答对了吗?其实关于堆栈内存的面试题,在开始不太熟练的时候只需要画图肯定会得到结果,当你熟练之后这张图不用画就会清晰的呈现在你的大脑里面。
参考
-
https://juejin.cn/post/6844903999183781895 -
https://www.javascriptpeixun.cn/course/3797/task/250471/show# -
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Memory_Management -
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/Operator_Precedence -
https://note.youdao.com/web/#/file/3F865CB39E2944D09D522CADB7D073A2/markdown/3F3ED9B88C6F46B5B85CBE168830BA3D/
作者介绍