0%

深入理解JavaScript之执行上下文和执行栈

都说想成为出色的 JavaScript 开发者,就要深入学习 JavaScript 程序内部的执行机制,最近学了一遍 JS 的执行上下文和执行栈,以此作总结。

首先先来了解几个专业概念

  • EC:函数执行环境(或执行上下文),Execution Context

  • ECS:执行环境栈,Execution Context Stack

  • VO:变量对象,Variable Object

  • AO:活动对象,Active Object

  • scope chain:作用域链

什么是执行上下文

每次当控制器转到 ECMAScript 可执行代码的时候,它都是在执行上下文中运行,即是指当前执行环境中的变量、函数声明,参数,作用域链,this 等信息。

组成代码示例

1
2
3
4
5
const ExecutionContextObj = {
VO: window, // 变量对象
ScopeChain: {}, // 作用域链
this: window,
}

执行上下文的类型

JavaScript 中有三种执行上下文类型。

  1. 全局执行上下文—— 这是默认上下文,浏览器中的全局对象就是 window 对象,任何不在函数内部的代码都在全局上下文中,this 指向这个全局对象。

  2. 函数执行上下文 —— 当函数被调用时创建,会为该函数创建一个新的执行上下文,可以有任意个。

  3. Eval 函数执行上下文 ——
    执行 eval 函数内部的代码也有属于它的上下文,由于开发中是尽量避免或不用 eval 函数,故此不作讨论。

执行栈

执行栈,也叫调用栈,被用来存储代码运行时创建的所有执行上下文。

栈:一种数据结构,遵循后进先出的原则

当 JavaScript 引擎第一次遇到脚本时,它会创建一个全局的执行上下文并且压入当前执行栈。每当引擎遇到一个函数调用,它会为该函数创建一个新的执行上下文并压入栈的顶部。

引擎会执行那些执行上下文位于栈顶的函数。当该函数执行结束时,执行上下文从栈中弹出,控制流程到达当前栈中的下一个上下文。

1
2
3
4
5
6
7
8
9
10
11
12
function fn1() {
console.log('fn1被调用了 -- 创建了fn1的函数执行上下文,压入栈')
fn2()
console.log('fn2执行完成,fn2的执行上下文会从栈中弹出')
}

function fn2() {
console.log('fn2被调用了 -- 创建了fn2的函数执行上下文,压入栈')
}

fn1()
console.log('fn1执行完成,fn2的执行上下文会从栈中弹出')

运行结果:

1
2
3
4
fn1被调用了 -- 创建了fn1的函数执行上下文,压入栈
fn2被调用了 -- 创建了fn2的函数执行上下文,压入栈
fn2执行完成,fn2的执行上下文会从栈中弹出
fn1执行完成,fn2的执行上下文会从栈中弹出

上述代码的执行上下文栈:

当上述代码在浏览器加载时,JavaScript 引擎创建了一个全局执行上下文并把它压入栈中,当函数 fn1()被调用时,JavaScript 为该函数创建了一个函数执行上下文,并把它压入当前执行栈的顶部。

当 fn1()函数内部调用 fn2()函数时,JavaScript 引擎同样创建了 fn2()的函数执行上下文并压入栈的顶部。然后执行了 fn2()函数后,fn2()函数会从当前栈(后进先出结构)弹出,并且按程序执行顺序继续执行 fn1()函数,即此刻处于 fn1 的函数执行上下文。

当 fn1()函数执行完毕,它的执行上下文从栈弹出,控制流程到达全局执行上下文。一旦所有代码执行完毕,JavaScript 引擎从当前栈中移除全局执行上下文。

执行上下文的创建

已经知道 JavaScript 怎样管理执行上下文了,现在来了解 JavaScript 引擎是怎么创建执行上下文的。

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

  1. 创建阶段
  2. 执行阶段。

在 JavaScript 代码执行前,执行上下文将经历创建阶段。在创建阶段会发生三件事:

  1. this 绑定
  2. 创建(LexicalEnvironment)词法环境组件
  3. 创建(VariableEnvironment)变量环境组件

执行上下文在概念可表示为:

1
2
3
4
5
ExecutionContext = {
ThisBinding = <this value>,
LexicalEnvironment = { ... },
VariableEnvironment = { ... },
}

1. 创建阶段

this 绑定

在全局执行上下文中,this 的值指向全局对象。(在浏览器中,this 引用 window 对象)

在函数执行上下文中,this 的指向取决于函数是如何被调用的,在本篇暂不对 this 指向做详细讨论。

1
2
3
4
5
6
7
8
9
let obj = {
fn: function () {
console.log(this)
},
}
let win = obj.fn

obj.fn() //this指向obj
win() // this指向window

词法环境(Lexical Environment)

ES6 官方文档把词法环境定义为:

词法环境是用来定义 基于词法嵌套结构的 ECMAScript 代码内的标识符与变量值和函数值之间的关联关系 的一种规范类型。一个词法环境由环境记录(Environment Record)和一个可能为 null 的对外部词法环境的引用(outer)组成。一般来说,词法环境都与特定的 ECMAScript 代码语法结构相关联,例如函数、代码块、TryCatch 中的 Catch 从句,并且每次执行这类代码时都会创建新的词法环境。

可以理解为词法环境是一种包含标识符(变量/函数的名称)和变量(函数/原始值/数组对象等)映射的数据结构

词法环境有两个组成部分
  1. 声明式环境记录器:存储变量和函数声明的实际位置

  2. 对象环境记录器:可以访问其外部词法环境(作用域)

词法环境有两种类型
  1. 全局环境:是一个没有外部环境的词法环境,其外部环境引用为 null。拥有一个全局对象(window 对象)及其关联的方法和属性(例如数组方法)以及任何用户自定义的全局变量,this 的值指向这个全局对象。

  2. 函数环境:用户在函数中定义的变量被存储在环境记录中,包含了 arguments 对象。对外部环境的引用可以是全局环境,也可以是包含内部函数的外部函数环境。

变量环境 (VariableEnvironment)

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

在 ES6 中,词法环境组件和变量环境组件之间的一个区别是前者用于存储函数声明和变量 let 和 const 绑定,而后者仅用于存储变量 var 绑定。

2.执行阶段

在此阶段,完成对所有这些变量的分配,最后执行代码。(在执行阶段,如果 JavaScript 引擎不能在源码中声明的实际位置找到 let 变量的值,它会被赋值为 undefined)

-------------本文结束感谢您的阅读-------------