介绍在js中执行上下文的运行规则(js预编译及执行流程)

94 阅读5分钟

今天我们来详细、系统地聊聊 JavaScript 中执行上下文的运行规则。这是理解 JavaScript 核心机制(如作用域、闭包、变量提升等)的关键。

首先我们要了解,什么是执行上下文? 执行上下文 是 JavaScript 代码被解析和执行时所在环境的抽象概念。每当代码运行时,它都会在一个执行上下文中进行。

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

  1. 全局执行上下文:这是默认的、最外层的上下文。在浏览器中,它是window对象;在 Node.js 中,它是 global 对象。一个程序中只有一个全局上下文。
  2. 函数执行上下文每次调用函数时,都会为该函数创建一个新的函数执行上下文。每个函数都有自己的执行上下文。

执行栈(调用栈)

为了管理这些执行上下文的创建和销毁,JavaScript 引擎使用了一个执行栈(也称为“调用栈”),预编译阶段创建执行栈

image.png

规则:执行栈是后进先出的数据结构,可以理解为一个只有上方开口的容器,在放入和取出数据时,得遵循先进后出的原则

image.png

过程

当代码开始执行时,JavaScript 引擎首先将全局执行上下文压入栈底。
每当一个函数被调用时,引擎就会为该函数创建一个新的函数执行上下文并将其压入栈顶。
v8引擎总是执行位于栈顶的当前执行上下文中的代码。
一旦该函数执行完毕,它的执行上下文就会从栈顶弹出,控制权交还给下一个(栈中的上一个)执行上下文。

image.png

预编译和执行阶段(执行上下文的生命周期)

每个执行上下文(包括全局和函数)的创建和运行都分为两个明确的阶段:

  1. 预编译阶段
  2. 执行阶段

阶段一:预编译阶段(在代码执行之前)

1. 创建变量对象

当全局被编译

  1. 创建全局执行上下文 GO对象
  2. 找var变量声明,将由 var 声明的变量名作为GO的属性,值为undefined
  3. 找函数体声明,函数名作为GO的属性,值为函数体

当函数体被编译

  1. 创建函数的执行上下文 A0对象
  2. 找形参和找到所有由 var 声明的变量声明,将形参和由 var 声明的变量声明作为A0的属性名,值为undefined
  3. 将实参和形参统一
  4. 在函数体内找函数声明,函数名作为A0的属性名,值为函数体,其值指向该函数在内存中的地址

这个机制就是大家熟知的变量提升

  1. 建立作用域链
作用域链是一个对象列表,用于变量标识符的解析。
每个执行上下文都会包含其外部环境(词法环境)的引用。当引擎在当前的变量对象中找不到变量时,它会沿着作用域链向外查找。
作用域链是在函数定义时就确定的,而不是调用时。这就是词法作用域。

image.png

阶段二:执行阶段

在这个阶段,代码开始逐行执行,引擎会:

按顺序执行代码。
执行到变量赋值时,将值赋给变量对象中对应的属性。
如果遇到未声明的变量,会在作用域链中查找,如果全局也找不到,则会抛出错误。

举例分析:理解两个阶段

让我们通过一段代码来直观感受这两个阶段:

var a = 2
function foo(){
var c = 3
}
foo()

1. 全局上下文的预编译阶段:

变量对象(GO):

1.创建全局执行上下文 GO对象
2.扫描变量声明,将变量名作为GO的属性,值为undefined  //找到a,现有GO:{a:undefined}    
3.扫描函数体声明,函数名作为GO的属性,值为函数体     //找到foo,现有GO:{a:undefined,foo:foo的函数体}
此时,全局上下文看起来像这样:
  GO: {
    foo: <function foo>,
    a: undefined
  },
  ScopeChain: [global GO],
}

image.png

2. 全局上下文的执行阶段:

  1. 执行 var a = 1;,将 GO 中的 a 赋值为 1。
  2. 执行 foo(2);,调用函数 foo。

image.png 3. 函数 foo 上下文的预编译阶段:

变量对象(称为“活动对象 AO”):

  1.创建函数的执行上下文 A0对象
  2.找形参和变量声明,将形参和变量声明作为A0的属性名,值为undefined //找到c,现有AO:{c:undefined}
  3.将实参和形参统一   //得到AO:{c:undefined}
  4.在函数体内找函数声明,函数名作为A0的属性名,值为函数体 //未找到
  

image.png

此时,foo 的上下文看起来像这样:

FunctionExecutionContext_foo = {
  AO: {
    c: undefined,
  },
  ScopeChain: [foo AO, global AO]
}

4. 函数 foo 上下文的执行阶段:

  1. 执行 var c = 3;,将 AO 中的 c 赋值为 3。

image.png

关键要点总结

  1. 执行顺序由执行栈管理:后进先出。
  2. 创建先于执行:代码运行前,上下文已经创建好,变量和函数已经“就位”。
  3. 变量提升的根源:var 声明的变量在创建阶段被初始化为 undefined,而函数声明则完整创建。因此函数可以在声明前调用,而 var 变量在赋值前使用会是 undefined。
  4. let 和 const 的不同:在 ES6 中,let 和 const 也存在提升,但它们被置于“暂时性死区”中,从块开始到声明语句之前的区域无法访问,这避免了 undefined 值的混淆。