一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第2天,点击查看活动详情。
前言
在前端来说,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 } }
你答对了吗?其实关于堆栈内存的面试题,在开始不太熟练的时候只需要画图肯定会得到结果,当你熟练之后这张图不用画就会清晰的呈现在你的大脑里面。