前端基石:Stack、Heap

·  阅读 657
前端基石:Stack、Heap

一起养成写作习惯!这是我参与「掘金日新计划 · 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 堆内存

栈内存的作用

  1. 代码的执行环境,将不同地方的代码放置在不同的执行上下文中执行。
  2. 存储原始值类型的值。
  3. 提供的变量对象(VO/GO)存储当前上下文中声明的变量。

堆内存的作用

  1. 存储对象的值,只要是引用类型,就会在 Heap 中开辟空间(16进制地址)来存储对象的键值对(或者函数的代码字符串)。

举个例子

当有如下代码,在浏览器环境中执行。当「声明一个变量等于一个值时」。浏览器会做哪些事情了。

// 全局执行上下文
let a = 1;
var b = 2;
let c = {
    name: 'stone',
    age: 13
};
复制代码

let 变量 = 值; 时,浏览器会进行三步操作:

  1. 创建值(原始类型直接存储在栈中,对象类型存储在堆中)。
  2. 声明变量,在变量对象中声明一个变量。
  3. 关联变量和值,这个操作称之为定义(赋值)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 代码执行会有如下操作。

  1. 首先在栈内存,VO(G)中创建值 1 并关联变量 a;
  2. 然后在堆内存,GO 中创建值 2 并关联变量 b;
  3. 再然后在堆内存,开辟一块新的内存空间,假设内存地址是 0x 001,存储 「name: 'stone', age: 13」;
  4. 接着在栈内存,声明变量 c ;
  5. 最后将变量 c 和内存 0x 001 关联起来。

「全局上下文」变量的访问和赋值

「全局上下文」访问变量

  1. 首先查看 VO(G) 中是否存在变量,如果有就是全局变量。
  2. VO(G) 没有,在基于 window 查看 GO 有没有,有则是全局对象的一个属性。
  3. 如果 GO 中也没有,就会报错“xxx is not defined”

「全局上下文」赋值变量:a = 100

  1. 首先查看赋值变量是否是全局变量,是就修改属性值。
  2. 如果没有就直接给 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 变量 = 值; 时,浏览器会进行三步操作:

  1. 创建值(原始类型直接存储在栈中,对象类型存储在堆中),所以在堆内存中开辟一块新的内存空间,假设内存地址是 0x001,用来存储 n: 1。
  2. 声明变量,在变量对象中声明一个变量 a 。
  3. 关联变量和值,将 a 和内存地址 0x001 关联起来。

let b = a;

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

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

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

画完堆栈内存的关系图,对于输出的结果就很明显了。

console.log(a.x); // undefined
console.log(b); // { n:1 x: { n: 2 } }
复制代码

你答对了吗?其实关于堆栈内存的面试题,在开始不太熟练的时候只需要画图肯定会得到结果,当你熟练之后这张图不用画就会清晰的呈现在你的大脑里面。

参考

分类:
前端
分类:
前端
收藏成功!
已添加到「」, 点击更改