理解 JavaScript 的执行上下文这篇就够了!

7,809 阅读6分钟

相信我,当理解了执行上下文后你会对 JavaScript 诸如作用域、闭包的概念豁然开朗(如若还有疑惑的话🤣)。

执行上下文发挥着什么作用?

JavaScript 里面的代码应该是顺序执行的吧?

你也许会这样认为。但一个很简单的例子就可以说明,JavaScript 代码在执行时实际上会发生一些鲜为人知的事:

num = 3;
var num;
console.log(num);//输出 3

num 变量在声明之前就被赋值,居然没有报错???

这是因为 var num;num = 3; 是属于代码运行的两个不同阶段的任务—— 编译阶段执行阶段 。编译发生在执行之前,所以 var num; 先被执行,num = 3 便不被报错了。

声明语句(var num)最先被执行好比将声明语句 “移动” 至当前作用域的顶端,这个过程被称作提升。而提升与执行上下文的关系则十分密切(见后文)。

执行上下文的概念

当一段 JavaScript 代码在运行的时候,它实际上是运行在执行上下文中。而下列三种代码都会创建相应的执行上下文:

  • 全局执行上下文:它是为运行代码主体而创建的执行上下文,也就是说它是为那些存在于函数之外的任何代码而创建的。
  • 函数执行上下文:每个函数会在执行的时候创建自己的执行上下文。
  • Eval 函数执行上下文:使用 eval() 函数也会创建一个新的执行上下文。

好吧,说了那么几个类型的执行上下文,它们是怎么被创建的呢?

执行上下文的创建

创建执行上下文有明确的几个步骤:

  1. 确定 this,即我们所熟知的 this 绑定。
  2. 创建 词法环境(LexicalEnvironment) 组件。
  3. 创建 变量环境组件(VariableEnvironment) 组件。

可以用伪代码这样定义一个执行上下文:

ExecutionContext = {
    // this
    ThisBinding = <this value>
    //词法环境
    LexicalEnvironment = { ... },
    //变量环境
    VariableEnvironment = { ... },
}

确定 this

在全局执行上下文中,this 总是指向全局对象。例如:浏览器环境下 this 指向 window 对象。

捕1获.PNG

在函数执行上下文中,this 的值取决于函数的调用方式,如果被一个对象调用,那么 this 指向这个对象。否则(在浏览器中) this 一般指向全局对象 window 或者 undefined (严格模式)。

var name = "window";
let hello = {
    name:"hello",
    helloThis: function() {
        console.log(this.name);
    }
}
  
hello.helloThis(hello);// window -> hello -> helloThis。由 hello 调用

let ht = hello.helloThis;
ht();//window -> helloThis,由 window 调用

捕1获.PNG

创建词法环境组件

词法环境是一个包含标识符变量映射的结构,这里的标识符表示变量(函数)的名称,变量是对实际对象(包括函数类型对象)或原始值的引用。

如:var name = 1;。标识符是 name,引用是 1

词法环境由环境记录器与对外部环境的引用两个组件组成。

  • 环境记录器用于存储当前环境中的变量和函数声明的实际位置。

  • 外部环境的引用对应着可以访问的其它外部环境。(所以子作用域可以访问父作用域)

执行上下文有全局执行上下文与函数执行上下文两种,而词法环境也有两种:

  • 全局环境中是对象环境记录器,没有外部环境引用(为 null )。它拥有内建的 Object、Array 等、在环境记录器内的原型函数(关联全局对象,比如 window 对象)和任何用户定义的全局变量,并且 this 的值指向全局对象。

  • 函数环境中是声明式环境记录器,存储着函数内部定义的变量。并且引用的外部环境可能是全局环境,或者任何包含此函数的外部函数环境。它还包含了用户在函数中定义的所有属性方法外和一个 arguments 对象和传递给函数的参数的 length

两种词法环境的伪代码:

//全局环境
GlobalExectionContext = {
    //词法环境
    LexicalEnvironment: {
        EnvironmentRecord: {
            //对象环境记录器
            Type: "Object",
          // 在这里绑定标识符
        }
        outer: <null>
    }
}
//函数环境
FunctionExectionContext = {
    //词法环境
    LexicalEnvironment: {
        //环境记录器
        EnvironmentRecord: {
            Type: "Declarative",
            //在这里绑定标识符
        }
        //外部环境的引用
        outer: <全局环境或包含该函数的外部函数环境>
  }
}

创建变量环境

变量环境与词法环境十分相似。在 ES6 中,词法环境和变量环境的明显不同就是前者被用来存储函数声明和变量(let/const)的绑定,而后者只用来存储 var 变量的绑定。

以下代码:

let a = 20;
const b = 30;
var c;

function multiply(e, f) {
    var g = 20;
    return e * f * g;
}

c = multiply(20, 30);

对应的伪代码:

GlobalExectionContext = {
  ThisBinding: <Global Object>,
  //词法环境
  LexicalEnvironment: {
    EnvironmentRecord: {
      Type: "Object",
      //存储 let/const 变量绑定
      a: < uninitialized >,
      b: < uninitialized >,
      multiply: < func >
    }
    outer: <null>
  },
  //变量环境
  VariableEnvironment: {
    EnvironmentRecord: {
      Type: "Object",
      // 在这里绑定标识符
      //存储并直接定义 var 变量
      c: undefined,
    }
    outer: <null>
  }
}

FunctionExectionContext = {
  ThisBinding: <Global Object>,
  LexicalEnvironment: {
    EnvironmentRecord: {
      Type: "Declarative",
      Arguments: {0: 20, 1: 30, length: 2},
    },
    outer: <GlobalLexicalEnvironment>
  },

  VariableEnvironment: {
    EnvironmentRecord: {
      Type: "Declarative",
      //存储并直接定义 var 变量
      g: undefined
    },
    outer: <GlobalLexicalEnvironment>
  }
}

解决疑问

为什么变量提升和执行上下文关系密切?

想必你已答得上来:执行上下文创建时,var 定义的变量会被设置为 undefined,而 let/const 定义的变量则未设置任何值(未初始化)。所以,如果在声明之前访问 var 变量将得到值 undefined,访问 let/const 定义的变量就会提示Cannot access 'b' before initialization

闭包是什么原理?

闭包是由 函数 以及声明该函数的 词法环境 组合而成的。词法环境存储着父级词法环境(作用域)的引用哦!

执行上下文的执行

在执行上下文中运行/解释函数代码,并在代码逐行执行时分配变量值。

值得注意的是,let 定义的变量(只有 let )如果为赋值,此阶段将赋值为 undefined

执行上下文的管理(执行栈)

既然每个函数执行时都会产生相应的执行上下文。如果多的上下文该如何管理呢?

执行栈存储着所有执行上下文,并遵循着后进先出的原则……看来来段代码实在的多:

var say = function(){
    hello();
}

var hello = function(){
    console.log("Hello,world!");
}

say();
  1. 创建全局执行上下文,压入执行栈中。
  2. 调用了 say函数,创建 say函数的执行上下文,并压入执行栈中。
  3. 进入 say函数 内部,调用了 hello函数,创建 hello函数的执行上下文,并压入执行栈中。
  4. hello函数执行完了,将 hello 移出执行栈
  5. say函数执行完了,将 say 移出执行栈

未标题-2.png

总结一下

捕获.PNG

好啦!终于写完了,各位大佬们赏个赞bie~👍