回归基本功—JS中的预编译与执行上下文

158 阅读7分钟

JavaScript预编译机制与执行上下文

在JavaScript的世界里,预编译和执行上下文(Execution Context)是理解代码执行流程和变量作用域的关键概念。这两个机制共同决定了变量、函数的生命周期以及它们如何在不同作用域中被访问。本文将详细探讨执行上下文的创建过程、预编译机制的工作原理,以及调用栈如何管理函数调用关系,帮助你更深入地理解JavaScript的运行机制。

预编译

预编译,也称为提升(Hoisting),是JavaScript引擎在执行任何代码之前进行的一项操作,它涉及变量和函数声明的处理。这一过程确保了在代码实际执行前,所有声明都被解析并准备好。预编译是执行上下文建立的一个前期准备工作

  1. 变量声明提升:JavaScript引擎会扫描代码,查找所有的变量声明(使用varletconst关键字声明的变量,以及function声明的函数),并将它们提升到当前作用域的最顶部。需要注意的是,letconst虽然也会提升,但不会初始化赋值,且在初始化之前访问它们会引发错误。
  2. 函数声明提升:使用function关键字声明的函数会被提升到所在作用域的顶部,这意味着在函数声明之前就可以调用该函数。
  3. 确定this的值:对于不同类型的执行上下文(全局、函数、eval),this的值会在预编译阶段被确定。
  4. 创建词法环境:为执行上下文创建词法环境记录,初始化作用域链,准备变量和函数名的绑定。

例如:在以下代码中console.log(a)语句不会报错,而是输出undefined,这时因为在预编译时由var声明的变量得到了提升,printA()的执行也是如此,函数也会得到提升。

console.log(a)  //  undefined
printA()   // "A"

var a = 1;
function printA(){
    console.log('A')
}

执行上下文

执行上下文是JavaScript解释器执行代码时所处的环境,它定义了变量或函数的访问范围。每个函数调用都会创建一个新的执行上下文,而全局代码则运行在一个默认的全局执行上下文下。

全局执行上下文

全局执行上下文是JavaScript程序启动时创建的第一个执行环境,它代表了整个脚本文件或Node.js模块的顶级执行环境。

全局执行上下文的创建过程大致如下:

  1. 初始化全局执行上下文对象(GAO, Global Activation Object) :在浏览器环境下,GAO通常关联于window对象;而在Node.js环境中,则对应于global对象。GAO是存储全局变量、函数以及其他环境特定信息的核心容器。
  2. 变量声明处理:JavaScript引擎扫描源代码,找出所有的全局变量声明(使用varletconst或直接赋值声明的),并在GAO中为它们创建属性条目,初始化值根据声明类型而定(如var声明的变量初始值为undefinedletconst声明的变量在预编译阶段不会赋值,处于“暂时性死区”)。
  3. 函数声明处理:查找所有的函数声明(不包括函数表达式),将函数名作为属性名添加到GAO中,属性值为函数的定义体。这一过程确保了函数可以在声明它们之前被引用。

函数执行上下文

每当一个函数被调用时,一个新的执行上下文会被创建,形成一个独立的执行环境,称为函数执行上下文。

函数执行上下文的创建过程大致如下:

  1. 生成执行上下文对象(AO, Activation Object) :为当前函数创建一个特殊的对象AO,用于存储函数内部的变量、参数以及内部函数声明。
  2. 参数与局部变量初始化:首先,将函数调用时传入的实参与函数定义的形参进行匹配,将形参名及对应的实参值作为属性添加到AO中,同时查找函数内部的所有局部变量声明(使用varletconst),同样作为AO的属性初始化(var声明的变量值为undefinedletconst同样在实际执行前不分配内存,不初始化)。
  3. 函数声明的再处理:在函数体内部,如果有函数声明,这些声明会被再次检查,并覆盖之前可能由变量声明创建的同名属性。此时,函数名作为AO的属性,值为函数的定义体,确保函数可以在其作用域内被正确引用和调用。
  4. 作用域链与this值的确定:最后,为当前函数执行上下文设置作用域链,即AO链接到外部作用域的链条,用于变量查找。同时,确定函数的this值,依据调用方式(普通函数调用、构造函数调用、方法调用、箭头函数等)决定。

代码的执行环境

每一次执行上下文的产生都包含了其变量环境和词法环境的产生

  • 变量环境:变量环境特指存储变量和函数声明的地方,它主要用于处理var声明的变量和function声明的函数。变量环境主要关注的是变量声明周期的管理,比如变量提升(hoisting)现象就是由变量环境的特性所决定的。但在ES6后ECMAScript规范将变量环境的职责合并进了词法环境

  • 词法环境:词法环境是ECMAScript规范中更为广泛和重要的一个概念,它不仅包含了变量环境的功能,还负责管理作用域链和闭包相关的行为。词法环境由两部分组成:环境记录和对外部词法环境的引用。

    • 环境记录:保存了当前作用域内的变量、函数声明以及函数参数等信息。每个词法环境都有自己的环境记录,用于存储在其作用域内的标识符(变量名、函数名等)到对应值的映射。
    • 外部词法环境的引用:形成了作用域链的基础,使得在当前环境中可以访问到外部环境的变量。这体现了JavaScript的词法(静态)作用域规则,即在编写代码时就能确定变量的作用域。

调用栈

  • 调用栈是一种数据结构,用于追踪当前执行的函数调用序列和它们的执行上下文。每当一个函数被调用时,一个新的执行上下文会被创建并压入调用栈;当函数执行完毕返回时,相应的执行上下文会被从栈顶弹出,控制权回到上一层函数。

  • 调用栈不仅帮助管理函数间的执行顺序,还决定了作用域链的构建。每个执行上下文都有自己的作用域链,它是由当前执行上下文的变量对象及其外部上下文的变量对象链接而成,用于变量查找规则。

通过执行上下文和预编译机制,JavaScript能够动态地管理代码执行的环境和变量的生命周期。预编译保证了变量和函数可以在声明它们之前被访问(尽管变量的实际值可能还是undefined),而执行上下文和调用栈则维护了代码执行的顺序和作用域规则。

理解这些底层机制对于编写高效、可维护的JavaScript代码至关重要,它能帮助开发者避免常见的作用域和变量声明错误,更加得心应手地运用JavaScript的动态特性。