一篇文章带你充分了解何为“执行上下文”

146 阅读7分钟

前言:

在阅读本文前,我们需要了解:

  • 全局环境:整个脚本的最外层环境,包含全局变量和函数。
  • 函数环境:每当函数被调用时创建的新环境,包含该函数的局部变量和参数。

1 执行上下文

1.1 什么是 执行上下文?

当 JS 代码开始执行时,JS 引擎首先会创建全局执行上下文,而全局执行上下文中包含全局对象(如浏览器中的window对象),所有的全局变量和函数都会作为这个对象的属性存在。

而当执行时需要调用函数时,就会为其创建函数执行上下文。而这个新的执行上下文会被推入到执行栈(Execution Stack)的顶部,成为当前正在执行的上下文。在函数执行完毕后,其对应的执行上下文会被从执行栈中弹出,控制权返回到之前的上下文。

而执行上下文就可以理解为代码在执行过程中的环境信息。

在 JS 中,运行环境主要包括全局环境和函数环境。 在 JS 代码运行过程中,最先进入的是全局环境,而在函数被调用时再进入相应的函数环境

在此我们将全局环境和函数环境所对应的执行上下文分别称为全局(执行)上下文和函数(执行)上下文。

执行上下文分为三类,即: 全局上下文函数上下文eval上下文。(由于eval(..)不推荐使用,所以就不深入探究了)

1.2 执行上下文 的生命周期:

1.2.1 生命周期的三个阶段:

  • 全局上下文的 创建阶段(Creation Phase)
    • 包含全局对象:例如在浏览器中是window,而在 Node.js 中是global
    • 初始化变量:所有声明的全局变量被初始化为undefined
    • 声明函数:所有函数声明会提升并初始化为对应的函数对象。
    • this绑定:在全局执行上下文中,this指向全局对象。
  • 全局上下文的 执行阶段(Execution Phase):
    • 逐行执行代码,处理变量赋值函数调用。
      • 创建 函数上下文:
        • 创建阶段:
          • 初始化变量对象:参数初始化、函数声明提升、变量声明提升。
          • 构建作用域链:包含当前执行上下文的变量对象及其父级执行上下文的变量对象。
          • 绑定this:根据函数的调用来确定this的值。
        • 执行阶段:逐行执行代码,处理变量赋值和函数调用等。
        • 销毁阶段:在函数执行完毕后进行销毁函数执行上下文。
  • 全局上下文的 销毁阶段(Destruction Phase)
    • 函数执行完毕后销毁执行上下文。

1.2.2 示例代码:

// 全局环境
var a = 1;

function q() {};
function foo(b){
    // 函数环境
    console.log(a);// 访问全局变量 a
    console.log(b);// 访问局部变量 b
    
    let c = 3;
    
    function fun(d){
        // 内层函数环境
    }
}

foo(2);

根据上述代码我们可以详细解释一下生命周期:

  1. 全局上下文的创建阶段:
    • 创建全局对象window
    • 初始化aundefined
    • 提升foo(..)q(..)function (直接console.log(q)可以得到[Function: q]的反馈)
  2. 全局上下文的执行阶段:
    • a被赋值为1
    • 调用foo(2),进入函数上下文的创建阶段。
  3. 函数上下文的创建阶段:
    • 创建变量对象VO(foo),包括:
      • 参数b初始化为2
      • 局部变量c初始化为undefined
      • 函数声明fun(..)提升为函数对象。
    • 构建作用域链,包含VO(foo)和全局变量对象。
    • 确定this值(假设普通函数调用,this指向全局对象window
  4. 函数上下文的执行阶段:
    • 访问a并打印。
    • 访问b并打印。
    • c赋值为3
  5. 函数上下文的销毁阶段:
    • 执行栈弹出foo的执行上下文,控制权返回到全局执行上下文。
  6. 全局上下文的销毁阶段:
    • 执行栈弹出全局的执行上下文,程序结束。

2 调用栈(Call Stack)

2.1 调用栈的基本概念

调用栈(也叫执行栈),其工作方式遵循后进先出(LIFO, Last In First Out)的原则,用于存储在执行代码时所创建的所有执行上下文。

首次运行 JS 代码时,会创建一个 全局上下文 并且将其 push(压入) 进当前的 执行上下栈,而每当一个函数被调用时,一个新的函数上下文会被创建并 push 进调用栈的顶部,当函数执行完毕后,相应的执行上下文会被从调用栈中pop(弹出),上下文控制权将移到当前执行栈的下一个执行上下文。

2.2 调用栈的工作原理:

由于我们并不能实际的显示出其如何工作的,所有在此我利用文字+代码+图片来为大家模型执行上下文栈的行为。

假设我们有以下JavaScript代码:

function math() {
    console.log(1); //输出 1
    function add(a,b){
        console.log(a + b);
    }
    console.log(add(1,2));// 输出 3
}

math();

为了模拟执行上下栈的行为,我们创建一个数组代表为执行上下栈:

var callStack = [];

我们知道在 JS 要开始执行代码时,最开始我们会遇到全局代码,所以我们初始化时,会先向执行上下栈压入一个全局上下文,我们不妨用global表示它,并且由于其后进先出原理,在我们结束代码之前全局上下文都会在栈底。

callStack.push('global');

在压入全局上下文后,我们就会遇到math(..)函数,当我们要执行其时,就会创建一个函数上下文,并且将其push进我们的执行上下栈callStack中,并且在运行结束后pop,所以我们运行代码就会发生以下处理。

// 执行math函数
callStack.push('math');

// 在math中还调用了add,所以我们还要创建add的函数上下文
callStack.psuh('add');

// 在add执行结束后,我们要将其弹出
callStack.pop();

// 执行完math后,同理也应当弹出
callStack.pop();

// 在这之中可以运行无数函数,但是底部的global纹丝不动。

a090f378ca1feda88ef1ed67daa3fc2c_720.jpg

3 变量提升:

3.1 变量提升的概念:

指使用var声明的变量以及函数声明会被提升到作用域顶部,而变量提升的结果,就是可以在变量初始化之前访问该变量(返回undefined),在函数声明前可以调用该函数。

例如:

console.log(a); // undefined
var a = 1;
console.log(a); // 1

在示例中虽然在代码还没执行到var a = 1时就运行console.log(a),但是不会报错,因为在代码执行之前就先进行了变量提升,会返还undefined

同理,函数声明也会被提升:

foo();// Hello world
function foo(){
   console.log("Hello world");
}

因为在代码执行之前就对foo()函数进行了声明,所以会正常运行。

3.2 变量提升的本质:

可以理解为在执行 JS 代码之前,需要先解析代码,而在解析的时候会先创建一个全局上下文,会先把代码中即将执行的变量、函数声明都拿出来,变量先赋值为undefined,函数先声明好可使用。这一步执行完了,才开始正式的执行程序。

3.3 注意事项:

  • letconst不会变量提升,要实现定义好。
  • 函数声明会提升,而函数表达式不会提升。

---欢迎各位点赞、收藏、关注,如果觉得有收获或者需要改进的地方,希望评论在下方,不定期更新,都看到这里了,真的忍心不点个赞吗~~~

0bae-hcffhsw0416753.gif