揭秘 JavaScript 调用堆栈
JavaScript 是一种单线程、单并发语言,这意味着它一次只能处理一个任务或一段代码。它只有一个调用堆栈,该调用堆栈与其他部分共同构成了 JavaScript 并发模型(在 V8 内部实现)。
本文将重点解释什么是调用堆栈,以及为什么它对 JavaScript 来说很重要并且是 JavaScript 需要它。
从最基本的层面上讲,调用堆栈是一种利用后进先出 (LIFO) 原则来存储和管理函数调用的数据结构。
由于调用栈是单一的,函数执行会从上到下逐个执行,从而实现调用栈的同步。在管理和存储函数调用时,调用栈遵循后进先出 (LIFO) 原则,这意味着最后一个被压入调用栈的函数执行总是会在调用栈弹出时被清除。
调用堆栈在 JavaScript 应用程序中起什么作用?JavaScript 如何利用这个特性?
当 JavaScript 引擎运行你的代码时,会创建一个执行上下文,这个执行上下文是第一个创建的执行上下文,它被称为Global Execution Context
。最初,这个执行上下文由两部分组成——一个全局对象和一个名为 的变量this
。
现在,当 JavaScript 中执行一个函数时(即在函数()
标签后使用 调用函数时),JavaScript 会创建一个名为 的新执行上下文local execution context
。因此,每次函数执行时,都会创建一个新的执行上下文
如果你想了解,执行上下文可以简单地理解为 JavaScript 代码执行的环境。执行上下文由以下部分组成:
- 执行线程和
- 本地记忆
由于 JavaScript 会创建一大堆执行上下文(或执行环境),而它只有一个线程,那么它如何跟踪线程应该位于哪个执行上下文以及应该返回到哪个执行上下文呢?我们简单地说call stack
。
当一个函数执行时,JavaScript 会为该函数创建一个执行上下文。新创建的执行上下文会被推送到调用栈。现在,调用栈顶部的内容就是 JavaScript 线程驻留的位置。最初,当 JavaScript 运行应用程序并创建 时global execution context
,它会将此上下文推送到调用栈。由于它似乎是调用栈中唯一的条目,因此 JavaScript 线程驻留在此上下文中并运行在其中找到的所有代码。
现在,在执行函数时,execution context
会创建一个新的函数,这一次local
,它被推入调用堆栈,并占据顶部位置,JavaScript 线程会自动移动到此处,运行在那里找到的指令。
JavaScript 知道,一旦遇到 return 语句或花括号,就应该停止执行函数。如果函数没有显式的 return 语句,则会返回undefined
,无论哪种情况,都会发生 return 。
因此,当 JavaScript 在执行函数的过程中遇到返回语句时,它立即知道这是函数的结束并删除已创建的执行上下文,同时,被删除的执行上下文将从调用堆栈中弹出,JavaScript 线程将继续执行到占据顶部位置的执行上下文。
为了进一步说明其工作原理,让我们看一下下面的代码片段,我将向我们展示其执行方式。
function randomFunction() {
function multiplyBy2(num) {
return num * 2;
}
return multiplyBy2;
}
let generatedFunc = randomFunction();
let result = generatedFunc(2);
console.log(result) //4
通过上面的小函数,我将说明 JavaScript 如何运行应用程序以及如何利用调用堆栈。
如果我们记得,JavaScript 第一次运行此应用程序时,全局执行上下文会被推送到调用堆栈中,对于上面的函数也会发生同样的事情,让我们来逐步介绍一下;
- 被
global execution context
创建并推入call stack
。 - JavaScript 在内存中开辟一块空间来保存函数定义,并赋值给一个标签
randomFunction
,此时函数只是定义,并没有执行。 - 接下来的 JavaScript 到达语句,
let generatedFunc = randomFunction()
由于尚未执行函数randomFunction()
,generatedFunc
因此等同于undefined
。 - 现在,JavaScript 遇到了括号,它表示一个函数即将被执行。它会执行该函数。我们之前记得,当一个函数被执行时,会创建一个新的执行上下文,这里也会发生同样的事情。一个我们可以调用的新执行上下文
randomFunc()
被创建,它被推入调用栈,占据栈顶位置,并将全局执行上下文(我们将在调用栈中进一步调用它)推global()
入栈底,从而使 JavaScript 线程驻留在该上下文中randomFunc()
。 - 由于 JavaScript 线程位于 内部
randomFunc()
,它开始运行在其中找到的代码。 - 它首先要求 JavaScript 在内存中为将分配给标签的函数定义腾出空间
multiplyBy2
,并且由于该函数multiplyBy2
尚未执行,因此它将移至返回语句。 - 当 JavaScript 遇到 return 关键字时,我们已经知道会发生什么,对吧?JavaScript 会终止该函数的执行,删除为该函数创建的执行上下文,并弹出调用栈,从而将函数的执行上下文从调用栈中移除。对于我们的函数,当 JavaScript 遇到 return 语句时,它会返回指示返回给下一个执行上下文的任何值,在本例中,它就是我们的
global()
执行上下文。
在语句 中return multiplyBy2
,需要注意的是,返回的不是标签,multiplyBy2
而是 的值multiplyBy2
。记住,我们之前要求 JavaScript 在内存中创建一个空间来存储函数定义,并将其赋值给标签multiplyBy2
。因此,当我们返回时,返回的是函数定义,并将其赋值给变量generatedFunc
,从而得到generatedFunc
如下所示的结果:
let generatedFunc = function(num) {
return num * 2;
};
现在我们说,JavaScript 应该在内存中为先前已知的函数定义创建一个空间,multiplyBy2
并将其分配给变量或标签generatedFunc
。
在下一行中,我们执行引用(之前的)let result = generatedFunc(2)
的函数定义,然后发生以下情况:generatedFunc
multiplyBy2
- 变量 result 等于,
undefined
因为此时它引用的函数尚未执行。 - JavaScript 创建了另一个我们称之为 的执行上下文
generatedFunc()
。当创建本地执行上下文时,它由本地内存组成。 - 在本地内存中,我们会将实参分配
2
给形参num
。 - 别忘了,本地执行上下文
generatedFunc()
会被推送到调用堆栈中,并且占据顶部位置,JavaScript 线程会运行在其中找到的所有代码。 - 当 JavaScript 遇到 return 语句时,它会评估
num * 2
,并且由于num
指的是2
最初存储在本地内存中,它会评估表达式2*2
并返回它。 - 在返回表达式的计算结果时
2*2
,JavaScript 会终止函数的执行generatedFunc
,返回值会存储在变量中result
,然后弹出调用堆栈,移除上下文,generatedFunc()
并将线程返回到global()
上下文。所以当我们 时console.log(result)
,我们得到了4
。
综上所述:
本文的关键要点是:
- 对于每个函数执行,都会创建一个新的执行上下文,该执行上下文会弹出到调用堆栈中,JavaScript 线程通过它来了解从哪个环境获取指令并执行。
感谢您的阅读。如果这篇文章对您有帮助,请点赞并分享,让更多人看到。我也很乐意看到您的评论。
本文使用的图片由FreecodeCamp