JavaScript 编译与作用域全解析:从 LHS/RHS 到词法作用域

107 阅读5分钟

JavaScript 编译原理与作用域深度解析

上期对var/let/const声明进行了深度的解析。今天我们在 JavaScript 的世界里,理解编译原理和作用域机制。很多开发者在面对变量提升、闭包、作用域链时感到困惑,其实这背后都有一套严格的运行机制。本文将按照从编译原理LHS/RHS 查询到作用域和词法作用域的顺序,带你系统理解 JavaScript 的执行逻辑。


一、JavaScript 的编译原理

与传统解释型语言不同,现代 JavaScript 引擎采用 即时编译(JIT, Just-In-Time) 技术。虽然 JS 是动态语言,但在执行之前,浏览器会对代码进行 编译优化,确保运行高效。JS 的编译阶段可以大致分为三个阶段:

  1. 词法分析(Lexical Analysis)

    • 将源代码拆分成一个个词法单元(Token) ,比如关键字、标识符、运算符等
    • 检查语法正确性,为后续语法分析做准备
  2. 语法分析(Syntax Analysis)

    • 将词法单元组织成 抽象语法树(AST)
    • 通过 AST,可以明确代码的执行结构和表达式嵌套关系
  3. 编译/执行上下文创建阶段(Execution Context Creation)

    • JS 会创建执行上下文,其中包括变量环境(Variable Environment)作用域链(Scope Chain)this 指针
    • 同时进行变量和函数声明提升,为 LHS/RHS 查询做好准备

在这个阶段,JavaScript 会将代码中的变量、函数信息提前收集并分配内存空间,这也是 变量提升 和函数声明提升的本质。

1.1 JavaScript 三个重要“角色”

在整个编译与执行过程中,有三个核心角色:

  1. JavaScript 引擎(Engine)

    • 浏览器或 Node.js 内置的执行环境
    • 负责管理内存、执行代码、调用编译器
  2. 编译器(Compiler)

    • 将源码解析为可执行的内部代码
    • 负责词法分析、语法分析、优化生成 AST
  3. 作用域(Scope)

    • 决定变量和函数的可访问范围
    • 通过作用域链来管理标识符查找

理解这三者的分工,可以帮助我们更好地理解变量提升、闭包和作用域链的机制。


二、LHS 与 RHS 查询

JavaScript 中,访问变量可以分为两种查询方式:

  1. RHS(Right-Hand Side)查询:查询的目的是获取变量的值
  2. LHS(Left-Hand Side)查询:查询的目的是对变量进行赋值

2.1 示例一:函数参数与 RHS 查询

function foo(a) {   // 注意:我们传参数其实也有一次隐式的LHS查询,我们将 2 赋值给a
  console.log(a); // RHS 查询,读取 a 的值
}

foo(2); // 输出 2

解释:

  • 当执行 console.log(a) 时,引擎进行 RHS 查询
  • 它会从当前执行上下文的作用域链中查找 a 的值
  • foo 被调用时,参数 a 已经在执行上下文中被创建,因此可以正确读取

2.2 再来一个例子

function foo(a) {   // 隐式的LHS查询:将 2 赋值给a
    var b = a;   //LHS:将a赋值给b,RHS:查询a的值
    return a + b;  //两处RHS:查询a和b的值
}
var c = foo(2);  //LHS:将foo(2)的返回值赋值给c,RHS:查询foo(2)的值
// 共有3出LHS,4处RHS

通过这种方式,LHS/RHS 查询机制可以清晰解释变量赋值与读取的底层原理。


三、作用域嵌套

JavaScript 中,每个函数都会创建一个 独立的执行上下文,并形成 作用域链(Scope Chain) 。当访问变量时,JS 会沿作用域链向上查找,直到全局作用域。

3.1 示例:作用域嵌套

var globalVar = "global";

function outer() {
  var outerVar = "outer";

  function inner() {
    var innerVar = "inner";
    console.log(globalVar); // global
    console.log(outerVar);  // outer
    console.log(innerVar);  // inner
  }

  inner();
}

outer();

解释:

  • inner 可以访问 outer 和全局变量
  • outer 无法访问 inner 的变量
  • 这体现了 作用域链向上查找 的规则

3.2 作用域链原理

当引擎执行 console.log(outerVar) 时,它会:

  1. inner 的执行上下文查找 outerVar
  2. 如果找不到,继续在 outer 的执行上下文查找
  3. 若仍找不到,最终在全局上下文查找
  4. 找不到则抛出 ReferenceError

四、词法作用域(Lexical Scope)

JavaScript 的作用域是 静态词法作用域,由代码写作位置决定,而不是运行时调用位置。

4.1 三层嵌套示例

var a = "global";

function A() {
  var b = "A";

  function B() {
    var c = "B";

    function C() {
      console.log(a, b, c);
    }

    return C;
  }

  return B;
}

var fnB = A();
var fnC = fnB();
fnC(); // 输出 "global A B"

解释:

  • 函数 C 的作用域链固定在 定义时
  • 即使函数在全局执行,依然可以访问 BA 和全局变量
  • 这就是闭包形成的基础

五、eval 与 with 的作用域“欺骗”

JavaScript 提供了两个特殊语法:evalwith,它们可以动态修改作用域,但也会带来困惑和性能问题。

5.1 eval

var x = 10;
eval("var y = 20;");
console.log(y); // 20,全局作用域被修改
  • eval 执行字符串代码,并且可以访问当前作用域
  • 它破坏了静态词法作用域,增加代码复杂度
  • 一般不推荐使用

5.2 with

var obj = {a: 1, b: 2};
with(obj) {
  console.log(a + b); // 3
}
  • with 将对象的属性临时添加到作用域链中
  • 会影响 LHS/RHS 查询,增加变量查找难度
  • 严格模式下禁止使用

六、总结

本文系统梳理了 JavaScript 的编译原理、LHS/RHS 查询、作用域嵌套与词法作用域,并讲解了 evalwith 的作用域特殊性:

  1. 编译原理:词法分析 → 语法分析 → 执行上下文创建
  2. LHS/RHS 查询:读取与写入变量的不同机制
  3. 作用域嵌套:变量查找沿作用域链向上,直至全局
  4. 词法作用域:作用域由代码书写位置确定,而非运行位置
  5. eval 与 with:动态修改作用域,容易引发潜在问题

掌握这些原理可以让你:

  • 理解变量提升和 TDZ
  • 写出可靠闭包和嵌套函数
  • 避免作用域相关陷阱

深入理解 JavaScript 的编译和作用域机制,你就能像“幕后导演”一样掌控代码执行,写出高质量、可维护的前端程序。