「前端每日一问(27)」JS 中执行上下文是什么?

1,610 阅读6分钟

Offer 驾到,掘友接招!我正在参与2022春招打卡活动,点击查看活动详情

本题难度:⭐ ⭐ ⭐

答:

执行上下文是 JavaScript 执行一段代码时的运行环境,包含了这段代码执行期间用到的诸如 this、变量、对象以及函数等环境信息。

只要有 Javascript 代码运行,那么它就一定是运行在执行上下文中。

执行上下文的类型分为三种:

  • 全局执行上下文:只有一个,浏览器中的全局对象就是 window 对象,this 指向这个全局对象
  • 函数执行上下文:存在无数个,只有在函数被调用的时候才会被创建,每次调用函数都会创建一个新的执行上下文
  • Eval 函数执行上下文: 指的是运行在 eval 函数中的代码,很少用而且不建议使用,本文不讨论它。

分析

环境是什么?人为什么生活在地球上而不是月球上,因为地球有人赖以生存的空气、水、食物等等东西,而月球没有。

代码运行的环境是什么?JS 代码要能正常执行,就得需要用到变量、作用域、this值等一系列信息,这些信息就是设计 JS 这门语言的人定义的规则,而一段代码执行所需的所有信息就被定义为执行上下文。

一段 JS 代码是以怎样的顺序解析和执行的?代码中的那些变量是何时被定义的?变量之间错综复杂的访问关系又是怎样创建和连接的?要解释这些问题,就必须了解 JS 执行上下文的概念。

执行上下文创建流程

当一段代码被执行时,JavaScript 引擎先会对其进行编译,并创建执行上下文。

首先,机器是无法直接识别高级语言代码的,要先转换成计算机能读懂的机器语言

而 JS 是一门解释型语言,转换代码的过程差不多如下图:

image.png

将源代码转换为抽象语法树的过程中,会生成执行上下文,也就是执行上下文的创建阶段,会在这个阶段确定 this值,创建 词法环境变量环境

image.png

比如,这样一段代码,

var x = 2
function sum(){
  var y = 10
  return  x + y
}
sum()

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

image.png

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

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

  • 首先,从全局执行上下文中,取出 sum 函数代码。
  • 其次,对 sum 函数的这段代码进行编译,并创建该函数的执行上下文和可执行代码。
  • 最后,执行代码,输出结果。

完整流程如下图:

image.png

拓展:结合伪代码分析创建阶段

执行上下文经历了很多版本的迭代,本文分析的是ES5版本中的执行上下文。

执行上下文创建阶段做了三件事:

  • 确定 this 的值,也被称为 This Binding
  • 词法环境(LexicalEnvironment)被创建
  • 变量环境(VariableEnvironment)被创建
// 伪代码
ExecutionContext = {  
  ThisBinding = <this value>,     // 确定this 
  LexicalEnvironment = { ... },   // 词法环境
  VariableEnvironment = { ... },  // 变量环境
}

确定 this

this的值是在执行的时候才能确认,定义的时候不能确认,

全局上下文的 this 指向 window。

函数上下文的 this 指向不是固定不变的,取决于函数处于什么位置、以什么方式调用,可以总结成如下图:

image.png

关于this,更多可参考我的这篇文章: 「前端每日一问(24)」说一下 JS 中的 this

词法环境

词法环境(lexical environment)是 JavaScript 引擎内部用来跟踪标识符与特定变量之间的映射关系。比如,看下面的代码:

var name = 'lin'
console.log(name)

当 console.log 语句访问 name 变量时,会进行词法环境的查询。

词法环境是 JS 作用域的内部实现机制,人们通常称为作用域(scope)。

词法环境有两个组成部分:

  • 全局环境:是一个没有外部环境的词法环境,其外部环境引用为 null,有一个全局对象,this 的值指向这个全局对象
  • 函数环境:用户在函数中定义的变量被存储在环境记录中,包含了arguments 对象,外部环境的引用可以是全局环境,也可以是包含内部函数的外部函数环境

伪代码如下:

// 伪代码
GlobalExectionContext = {
  LexicalEnvironment: {
    EnvironmentRecord: {
      Type: "Object",
      // 在这里绑定标识符
    }
    outer: <null>
  }
}

FunctionExectionContext = {
  LexicalEnvironment: {
    EnvironmentRecord: {
      Type: "Declarative",
      // 在这里绑定标识符
    }
    outer: <Global or outer function environment reference>
  }
}

变量环境

变量环境也是一个词法环境,因此它具有上面定义的词法环境的所有属性

在 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",  
      // 标识符绑定在这里  
      a: < uninitialized >,  
      b: < uninitialized >,  
      multiply: < func >  
    }  
    outer: <null>  
  },

  VariableEnvironment: {  // 变量环境
    EnvironmentRecord: {  
      Type: "Object",  
      // 标识符绑定在这里  
      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",  
      // 标识符绑定在这里  
      g: undefined  
    },  
    outer: <GlobalLexicalEnvironment>  
  }  
}

注意,letconst定义的变量ab在创建阶段没有被赋值,但var声明的变量从在创建阶段被赋值为undefined

这是因为,创建阶段,会在代码中扫描变量和函数声明,然后将函数声明存储在环境中。

但变量会被初始化为undefined(var声明的情况下)和保持uninitialized(未初始化状态)(使用letconst声明的情况下)。

这就是变量提升的实际原因。

参考文章:

[译] 理解 JavaScript 中的执行上下文和执行栈

结尾

本文完全是八股文,看不懂没关系,不影响继续做 API 工程师。

但看懂了还是有点帮助,给我的感受是,查阅了很多资料,把这些八股文的理念勉强搞懂之后,很多知识点都被串联起来了。

如果我的文章对你有帮助,你的👍就是对我的最大支持^_^

你也可以关注《前端每日一问》这个专栏,防止失联哦~

我是阿林,输出洞见技术,再会!

上一篇:

「前端每日一问(26)」箭头函数和普通函数有啥区别?

下一篇:

「前端每日一问(28)」说说你对闭包的理解