JS原理 - 理解执行上下文与调用栈

执行上下文

JS 引擎运行代码时,会先对代码进行编译,经过编译后会生成两部分内容:执行上下文(Execution context)和可执行代码。

执行上下文是 JS 执行一段代码时的运行环境,比如调用一个函数,就会进入这个函数的执行上下文,确定该函数在执行期间用到的诸如this、变量、对象以及函数等。

那么执行什么样的代码会创建执行上下文呢?一般来说有以下三种情况:

  1. 当执行全局代码的时候,会编译全局代码并创建全局执行上下文,而且在整个页面的生存周期内,全局执行上下文只有一份。

  2. 当调用一个函数的时候,函数体内的代码会被编译,并创建函数执行上下文,一般情况下,函数执行结束之后,创建的函数执行上下文会被销毁。

  3. 当使用eval函数的时候,eval的代码也会被编译,并创建执行上下文。

那么创建出的执行上下文又该如何进行统一的管理呢?这时候就要用到调用栈了。

调用栈

要弄清楚调用栈就要先弄明白函数调用和栈。

函数调用

用下述的代码来解释下函数调用的过程

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

在执行到函数add()之前,JS 引擎会为上面这段代码创建全局执行上下文,包含了声明的函数和变量。

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

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

  1. 从全局执行上下文中,取出 add 函数代码。

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

  3. 执行代码,输出结果。

完整流程如下图所示:

备注:上图中,编译代码过程var d=10应为var b=10,可执行代码除了return a+b还有b=10

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

栈相当于一个死胡同,栈内的所有元素必须遵守先进后出的原则

什么是调用栈?

调用栈亦称作执行栈,具有后进先出的结构,存储着代码执行期间创建的所有执行上下文。执行栈用于管理执行上下文,是 JS 引擎追踪函数执行的一个机制,通过执行栈可以追踪到哪个函数正在被执行以及各函数之间的调用关系。

栈的工作过程:

  1. 栈的底部最初会被压入全局执行上下文,所以只有当整个代码程序结束时,栈才会被清空;
  2. 当执行一个函数时,就会创建一个执行上下文,并压入执行上下文栈中,然后执行函数内的代码,当函数执行完毕时,其执行上下文会从栈顶弹出,并将函数返回值赋给调用函数的变量;
  3. 再次遇到函数时,重复步骤 2,直到全局代码执行完毕;

举个例子:

console.log("global execution context");

function foo() {
  console.log("foo is executing");
  console.log("foo has finished executing");
}

function bar() {
  console.log("bar is executing");
  foo();
  console.log("bar has finished executing");
}

function baz() {
  console.log("baz is executing");
  bar();
  console.log("baz has finished executing");
}

baz();
console.log("program successfully executed");

// global execution context
// baz is executing
// bar is executing
// foo is executing
// foo has finished executing
// bar has finished executing
// baz has finished executing
// program successfully executed

执行过程如下图所示:

如何利用调用栈?

调用栈可以帮助我们快速追踪到哪个函数正在被执行以及各函数之间的调用关系,有两种方式可以查看调用栈信息。

  1. 使用开发者工具

    打开“开发者工具”,点击 “Source” 标签,选择 JavaScript 代码的页面,然后在第3行加上断点,并刷新页面。你可以看到执行到 add 函数时,执行流程就暂停了,这时可以通过右边 “call stack” 来查看当前的调用栈的情况,如下图:

  2. 使用控制台打印使用console.trace()来打印出当前函数的调用关系

栈溢出

栈的容量是有限的,不能够无限的压入执行上下文,所以当出现如下述代码中的循环调用的时候,就容易出现栈溢出(Maximum call stack size exceeded)。

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

JS原理 - 理解执行上下文与调用栈

https://hashencode.github.io/post/52080/

作者

BiteByte

发布于

2022-07-18

更新于

2024-11-15

许可协议