likes
comments
collection
share

JavaScript 在浏览器中的执行机制系列之"调用栈"

作者站长头像
站长
· 阅读数 15

上文作为执行系列的第一篇重点介绍了 JavaScript 在浏览器执行时的变量提升。本篇主要介绍内容为调用栈

在介绍调用栈之前需要先明确一个上文没有明确介绍的概念-执行上下文。

什么是执行上下文

执行上下文包含下面两种:

全局执行上下文(GEC)

每当 JavaScript 引擎接收到脚本文件时,它首先会创建一个默认的执行上下文,称为 全局执行上下文 (GEC)。 GEC是基础/默认的执行上下文,所有 不在函数内部的JavaScript代码都在这里执行。

每个 JS 文件只会有一个全局执行上下文

函数执行上下文(FEC)

每当函数被调用时,JavaScript引擎就会在GEC内部创建另一种执行上下文,称为函数执行上下文(FEC),并在FEC中评估和执行函数中的代码。 因为每个函数调用都创建自己的FEC,所以在脚本运行期间会有多个FEC。

什么情况下会触发上线文

  • 当 JavaScript 执行全局代码时,会编译全局代码并创建全局上下文,并且只有一份。
  • 当调用一个函数时,会编译函数体内的代码并创建函数执行上下文,一般函数执行完成后创建的执行上下文会被销毁。
  • 当使用 eval 函数的时候,eval 函数的代码会被编译并创建执行上下文。

JavaScript 在浏览器中的执行机制系列之"调用栈"

执行上下文示意图

上面介绍完了执行上下文的概念了,全局执行上下文没有太多要说的,下面了解一下什么是函数调用。

什么是函数调用

函数调用简单的说就是运行一个函数,具体使用方式就是函数名后面带一个小括号,如下代码:

 var a = 2
 function add() {
	var b = 8
	return a + b
 }

 add()

上面代码很简单我们主要看一下函数调用的过程。在执行到 add() 函数之前已经创建了全局执行上下文,包含声明的函数和变量,如下图所示:

JavaScript 在浏览器中的执行机制系列之"调用栈"

全局执行上下文

从上图可看出函数调用前就准备好了全局上下文,然后全局变量和函数都在话术上下文中。当执行到 add() JS 引擎判断出是函数那开始执行下面操作:

  1. 从全局上下文中取出 add 函数代码
  2. add 函数代码进行编译并创建该函数的函数执行上下文和可执行代码。
  3. 执行代码并输出结果。

如下图所示:

JavaScript 在浏览器中的执行机制系列之"调用栈"

函数调用

上面提到了函数执行上下文可能有多个,那 JS 引擎是如何来管理的呢?是通过一种叫做栈的数据结构

什么是栈

栈(stack)又名堆栈,它是一种运算受限的线性表。限定仅在表尾进行插入和删除操作的线性表。这一端被称为栈顶,相对地,把另一端称为栈底。向一个栈插入新元素又称作进栈、入栈或压栈,它是把新元素放到栈顶元素的上面,使之成为新的栈顶元素;从一个栈删除元素又称作出栈或退栈,它是把栈顶元素删除掉,使其相邻的元素成为新的栈顶元素。

栈的特性是 后进先出

什么是 JavaScript 调用栈

调用栈是解释器(比如浏览器中的 JavaScript 解释器)追踪函数执行流的一种机制。当执行环境中调用了多个函数时,通过这种机制,我们能够追踪到哪个函数正在执行,执行的函数体中又调用了哪个函数。

  • 每调用一个函数,解释器就会把该函数添加进调用栈并开始执行。
  • 正在调用栈中执行的函数还调用了其他函数,那么新函数也将会被添加进调用栈,一旦这个函数被调用,便会立即执行。
  • 当前函数执行完毕后,解释器将其清出调用栈,继续执行当前执行环境下的剩余的代码。
  • 当分配的调用栈空间被占满时,会引发“堆栈溢出”错误。

根据上 MDN 的释义我们了解到 JS 引擎正是用了栈后进先出的这种特性来管理执行上下文的。

下面用一段代码来介绍调用栈:

代码中可以看出先调用了 addAll() 然后调用 add(), 那调用栈是如何变化的,如下图所示:

JavaScript 在浏览器中的执行机制系列之"调用栈"

调用栈

从上图就能明确看出调用栈是 JavaScript 引擎追踪函数执行的一种机制,当一次有多个函数被调用时,通过调用栈可以追踪到正在被执行的函数与各个函数之间的关系。

在项目中如何利用调用栈调试呢

浏览器查看调用栈信息

作为资深切图仔大家肯定对打断点熟能生巧了,但是大家并不知道你断点右侧的执行步骤其实就是我们上面介绍的调用栈,具体位置不详细说了,直接上图:

JavaScript 在浏览器中的执行机制系列之"调用栈"

浏览器查看调用栈

还可以通过 console.trace() 打印栈信息,如下图所示:

JavaScript 在浏览器中的执行机制系列之"调用栈"

console.trace

总结

  • 每调用一个函数,JavaScript引擎会为其创建执行上下文,并把该执行上下文压入调用栈,然后JavaScript引擎开始执行函数代码。
  • 如果在一个函数 A 中调用了另外一个函数 B,那么JavaScript引擎会为B函数创建执行上下文,并将B函数的执行上下文压入栈顶。
  • 当前函数执行完毕后,JavaScript引擎会将该函数的执行上下文弹出栈。
  • 当分配的调用栈空间被占满时,会引发“堆栈溢出”问题。