🐟 🐟从执行上下文到调用栈

155 阅读5分钟

前言

在上一节里首次谈到了执行上下文这个概念,现在再从这个概念中延伸到调用栈

上一篇文章在这哦 👉👉🐟 🐟 JavaScript执行机制之变量提升

(还是以李兵老师的内容为主了,这里也推荐大家去买他的课程,真的讲的很好) 👍👍08 | 调用栈:为什么JavaScript代码会出现栈溢出?

函数调用

还是老规矩,在介绍什么是调用栈之前,我们还得先搞明白什么是函数调用

函数调用就是运行一个函数,下面我们看个简单的示例代码:


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

那么下面我们就利用这段简单的代码来解释下函数调用的过程。

在执行到函数add()之前,JavaScript 引擎会为上面这段代码创建全局执行上下文,包含了声明的函数和变量,你可以参考下图:

image.png

从图中可以看出,代码中全局变量和函数都保存在全局上下文的变量环境中。

执行上下文准备好之后,便开始执行全局代码,当执行到 add 这儿时,JavaScript 判断这是一个函数调用,那么将执行以下操作:

  • 首先,从全局执行上下文中,取出 add 函数代码。

  • 其次,对 add 函数的这段代码进行编译,并创建该函数的执行上下文和可执行代码。

  • 最后,执行代码,输出结果。

完整流程你可以参考下图:

image.png

就这样,当执行到 add 函数的时候,我们就有了两个执行上下文了——全局执行上下文和 add 函数的执行上下文。

也就是说在执行 JavaScript 时,可能会存在多个执行上下文,那么 JavaScript 引擎是如何管理这些执行上下文的呢?

答案是通过一种叫栈的数据结构来管理的。

JavaScript 的调用栈

JavaScript 引擎正是利用栈的这种结构来管理执行上下文的。在执行上下文创建好后,JavaScript 引擎会将执行上下文压入栈中,通常把这种用来管理执行上下文的栈称为执行上下文栈,又称调用栈

为便于更好地理解调用栈,下面我们再来看段稍微复杂点的示例代码:


var a = 2
function add(b,c){
  return b+c
}
function addAll(b,c){
var d = 10
result = add(b,c)
return  a+result+d
}
addAll(3,6)

第一步,创建全局上下文,并将其压入栈底。如下图所示:

image.png

从图中你也可以看出,变量 a、函数 addaddAll 都保存到了全局上下文的变量环境对象中。

全局执行上下文压入到调用栈后,JavaScript 引擎便开始执行全局代码了。首先会执行 a=2 的赋值操作,执行该语句会将全局上下文变量环境中a的值设置为 2。设置后的全局上下文的状态如下图所示:

image.png

接下来,第二步是调用 addAll 函数。当调用该函数时,JavaScript 引擎会编译该函数,并为其创建一个执行上下文,最后还将该函数的执行上下文压入栈中,如下图所示:

image.png addAll 函数的执行上下文创建好之后,便进入了函数代码的执行阶段了,这里先执行的是d=10的赋值操作,执行语句会将 addAll 函数执行上下文中的 d 由 undefined 变成了 10。

然后接着往下执行,第三步,当执行到 add 函数调用语句时,同样会为其创建执行上下文,并将其压入调用栈,如下图所示:

image.pngadd 函数返回时,该函数的执行上下文就会从栈顶弹出,并将 result 的值设置为add函数的返回值,也就是 9。如下图所示:

image.png 紧接着 addAll 执行最后一个相加操作后并返回,addAll 的执行上下文也会从栈顶部弹出,此时调用栈中就只剩下全局上下文了。最终如下图所示:

image.png 至此,整个 JavaScript 流程执行结束了。

栈溢出

还有一点你要注意,调用栈是有大小的,当入栈的执行上下文超过一定数目,JavaScript 引擎就会报错,我们把这种错误叫做栈溢出

特别是在写递归代码的时候,就很容易出现栈溢出的情况。比如下面这段代码:


function division(a,b){
    return division(a,b)
}
console.log(division(1,2))

当执行时,就会抛出栈溢出错误,如下图:

image.png

从上图可以看到,抛出的错误信息为:超过了最大栈调用大小

那为什么会出现这个问题呢?这是因为当 JavaScript 引擎开始执行这段代码时,它首先调用函数 division,并创建执行上下文,压入栈中;然而,这个函数是递归的,并且没有任何终止条件,所以它会一直创建新的函数执行上下文,并反复将其压入栈中,但栈是有容量限制的,超过最大数量后就会出现栈溢出的错误。

小结

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

本文正在参加「金石计划 . 瓜分6万现金大奖」