执行上下文和调用栈

448 阅读3分钟

这是我参与8月更文挑战的第23天,活动详情查看:8月更文挑战

执行上下文和调用栈这两个概念便一直伴随我们左右,我们每编写的每一行代码都和其息息相关,理解这两个隐藏在代码后的执行逻辑,有助于更好的编写代码

执行上下文

执行上下文就是当前代码的执行环境/作用域。直观上看,执行上下文包含了作用域链,同时它们又像是一条河的上下游:有了作用域链,才有了执行上下文的一部分。

代码执行的两个阶段

理解这两个概念,要从 JS 代码的执行过程说起,这在平时开发中并不会涉及,但对于我们理解 JS 语言和运行机制非常重要。JS 执行主要分为两个阶段:

  • 代码预编译阶段
  • 代码执行阶段

预编译阶段是前置阶段,这个时候由编译器将 JS 代码编译成可执行的代码。 注意,这里的预编译和传统的编译并不一样,传统的编译非常复杂,涉及分词、解析、代码生成等过程 。这里的预编译是 JS 中独特的概念,虽然 JS 是解释型语言,编译一行,执行一行。但是在代码执行前,JS 引擎确实会做一些“预先准备工作”。

执行阶段主要任务是执行代码,执行上下文在这个阶段全部创建完成。

在通过语法分析,确认语法无误之后,JS 代码在预编译阶段对变量的内存空间进行分配,我们熟悉的变量提升过程便是在此阶段完成的。如下代码:

经过预编译过程,我们应该注意三点:

  • 预编译阶段进行变量声明;
  • 预编译阶段变量声明进行提升,但是值为 undefined;
  • 预编译阶段所有非表达式的函数声明进行提升。

先看一段代码

console.log(address);
getAge();
var address = 'shanghai'
function getAge() {
  console.log(18)
}

通过控制台可以看到输出:undefined、18

输出结果和我们常说的JS代码是按照顺序执行是不一样的,因为作用域在预编译阶段确定,但是作用域链是在执行上下文的创建阶段完全生成的。因为函数在调用时,才会开始创建对应的执行上下文。执行上下文包括了:变量对象、作用域链以及 this 的指向

调用栈

调用栈是用来管理函数调用关系的一种数据结构: JS 引擎利用栈结构管理执行上下文,在执行上下文创建好后,JS 引擎会将执行上下文压入栈中,通常把这种用来管理执行上下文的栈称为执行上下文栈,又称调用栈。

function foo1() {
  foo2()
}
function foo2() {
  foo3()
}
function foo3() {
  foo4()
}
function foo4() {
  console.log('foo4')
}
foo1()

调用关系:foo1foo2foo3foo4。这个过程是 foo1 先入栈,紧接着 foo1 调用 foo2foo2入栈,以此类推,foo3foo4,直到 foo4 执行完 —— foo4 先出栈,foo3 再出栈,接着是 foo2 出栈,最后是 foo1 出栈。这个过程“先进后出”(“后进先出”),因此称为调用栈

正常来讲,在函数执行完毕并出栈时,函数内局部变量在下一个垃圾回收节点会被回收,该函数对应的执行上下文将会被销毁,这也正是我们在外界无法访问函数内定义的变量的原因。也就是说,只有在函数执行时,相关函数可以访问该变量,该变量在预编译阶段进行创建,在执行阶段进行激活,在函数执行完毕后,相关上下文被销毁。