执行上下文和执行上下文栈

执行上下文

当函数执行时,会创建一个称为执行上下文的变量对象可理解为作用域。一个执行上下文定义了一个函数执行时的环境。

执行上下文也分为两种类型:

  • 全局执行上下文: 代码首次执行时候的默认环境,在代码的整个执行过程中,只用一个全局执行上下文。

  • 函数执行上下文: 每当执行流程进入到一个函数体内部的时候,就会创建一个函数执行上下文,可以有任意数量的函数执行上下文

执行上下文主要有两个阶段: 创建阶段执行阶段

创建阶段

  • 首先,为每个函数或变量创建与外部环境的连接,这这个连接形成作用域链作用链告诉执行上下文它应该包含什么,以及它应该在哪里查找解析函数的引用和变量的值。

  • 扫描作用链后,将创建一个环境存储器,其中全局上下文变量函数及函数参数的创建和引用在内存中完成。

  • 最后,在第一步中创建的每个执行上下文中确定 this 关键字的值。

每个执行上下文包含了三个重要属性:变量对象(VO)作用域链this

创建阶段伪代码:

1
createContext = { // 创建阶段
2
  'outerEnvironmentConnection': { // 创建外部连接
3
        // 形成作用域链
4
   },    
5
   'variableObjectMapping': {
6
        // 变量、函数和函数参数的创建和引用在内存中完成。
7
   },
8
   'valueOfThis': {},  // 确定 this 的值
9
}

执行阶段

执行阶段是代码在创建阶段形成的执行上下文中的运行的阶段,并且逐行分配变量值。

当执行开始时,JavaScript 引擎在其创建阶段形成的AO对象中查找执行函数的引用。如果在当前对象中没有找到,它将沿着作用域继续向上查找,直到它到达全局环境。

如果在全局环境中找不到函数引用,则将返回错误。

如果找到了引用并且函数正确执行,那么这个特定函数的执行上下文将从栈中弹出,接着 JavaScript 引擎将移动到下一个函数,它们的函数执行上下文将被加入到栈中并执行,以此类推。

下面结合代码来理解两种执行上下文两个阶段

1
let name = "webinfoq";
2
var title = "execution context";
3
const message = "hello world";
4
5
function func1(num) {
6
  var author = "deepak";
7
  let value = 3;
8
  let func2 = function multiply() {
9
    return num * value;
10
  }
11
  const fixed = "Divine";
12
  function addFive() {
13
    return num + 5;
14
  }
15
}
16
func1(10);

因此全局执行上下文创建阶段将如下所示:

1
globalExecutionContext = {  // 全局执行上下文
2
    outerEnvironmentConnection: null,  // 全局上下文外部环境为 null
3
    variableObjectMapping: { 
4
        name: uninitialized,  // 在创建阶段,let 声明的变量是未初始化状态
5
        title: undefined,     // 在创建阶段,var 声明的变量表示为未定义
6
        date: uninitialized,  // 在创建阶段,const 声明的变量是未初始化状态
7
        func1: `<func1 reference>`,  // func1 地址引用
8
    },
9
    this: window // Global Object  
10
}

执行阶段完成对变量的赋值等操作:

1
globalExectutionContext = {  // 全局执行上下文
2
    outerEnvironmentConnection: null, // 全部上下文外部环境为 null
3
    variableObjectMapping: {
4
        name: "webinfoq",
5
        title: "execution context",
6
        message: "hello world",
7
        func1: pointer to function func1, // 指向 func1 的指针
8
    },
9
    this: window //Global Object
10
}

当执行到 func1 时,将形成新的函数执行上下文创建阶段如下所示:

1
func1ExecutionContext = {  // func1 函数执行上下文
2
    outerEnvironmentConnection: Global,  // 外部环境为全局环境
3
    variableObjectMapping: {
4
       arguments: {
5
            0: 10,
6
            length: 1
7
        },
8
        num: 10,
9
        author: undefined,  // var 声明的
10
        value: uninitialized,  // let 声明的
11
        func2: uninitialized,  // let 声明的
12
        fixed: uninitialized,  // const 声明
13
        addFive: `<addFive reference>`  // addFive 地址引用
14
    },
15
    this: Global Object or undefined  
16
}

执行阶段如下所示:

1
func1ExecutionContext = {
2
    outerEnvironmentConnection: Global,  
3
    variableObjectMapping: {
4
       arguments: {  // 先处理 arguments 参数
5
            0: 10,
6
            length: 1
7
        },
8
        num: 10,
9
        author: "deepak",  //变量赋值
10
        val: 3,
11
        func2: pointer to function func2() 
12
        fixed: "Divine"
13
        addFive: pointer to function addFive()
14
    },
15
    this: Global Object or undefined
16
}

最后

Javascript 引擎创建执行上下文,调用栈。当有函数执行时,引擎就会创建一个新的函数执行上下文。最后所用函数执行完成后,将更新全局环境,然后全局代码完成,程序结束。

执行上下文栈

通过阅读上一小节我们得知,每当有函数执行时,就会创建一个全新的执行上下文,那么怎么管理这些执行上下文呢?

JavaScript 引擎创建了执行上下文栈 Execution Context Stack 来管理执行上下文。

可以把执行栈认为成一个储存函数调用的栈结构,遵循先进后出的原则。

下图为执行上下文入栈出栈示意图:

入栈出栈示意图

JavaScript 引擎是单线程执行,所有代码都是排队执行:

  • 一开始执行的是全局代码,首先创建全局执行上下文,然后将该执行上下文压入执行栈中。

  • 当执行一个函数,就会创建该函数执行上下文,然后将其压入执行栈的顶部。

  • 函数执行完成后,执行上下文从底部退出,等待垃圾回收。

调用栈的应用:

1
你可以把调用栈想象成一个函数调用链,就像我们在前面代码段的注释中所写的一样。
2
另一个查看调用栈的方法是使用浏览器的调试工具。绝大多数现代桌面浏览器都内置了开发者工具, 其中包含 JavaScript 调试器。
3
就本例来说,你可以在工具中给 foo() 函数的第一行代码设置一个断点,或者直接在第一行代码之前插入一条 debugger 语句。
4
运行代码时,调试器会在那个位置暂停,同时会展示当前位置的函数调用列表,这就是你的调用栈。
5
因此,如果你想要分析 this 的绑定,使用开 发者工具得到调用栈,然后找到栈中第二个元素,这就是真正的调用位置。