《你不知道的JavaScript(上卷)》作用域是什么 & 词法作用域

84 阅读4分钟

你不知道的 JavaScript(上卷)第一章:作用域是什么

本文整理自《你不知道的 JavaScript(上卷)》第一、二章,旨在帮助读者从编译器视角理解 JavaScript 的作用域机制与词法作用域。


一、谁在负责查找变量?

在传统印象中,JavaScript 引擎似乎包揽了所有任务:执行代码、管理变量、处理内存。
但实际上,还有两个同样重要的参与者:

  • JS 引擎:负责编译并执行代码。
  • 编译器(Compiler) :负责语法分析与代码生成。
  • 作用域(Scope) :收集并维护所有声明的标识符,并制定变量访问规则。

关键点:JavaScript 并非完全“解释执行”,而是有一个即时编译(JIT)过程。


二、JavaScript 的编译过程(以 var a = 2 为例)

1. 分词 / 词法分析(Tokenizing / Lexing)

将代码分解为有意义的最小单元(token):

var | a | = | 2 | ;

2. 解析 / 语法分析(Parsing)

将 tokens 转换为抽象语法树(AST)。

3. 代码生成(Code Generation)

将 AST 转换为可执行的机器指令,引擎根据这些指令创建变量并执行赋值操作。


三、LHS 与 RHS 查询

LHS(Left-hand Side)查询

  • 出现在赋值操作左侧,用于查找赋值目标
  • 目标是找到变量的“容器”位置。
a = 2; // 找到 a 的存储位置 —— LHS 查询

RHS(Right-hand Side)查询

  • 出现在赋值右侧,用于获取变量的值
console.log(a); // 取 a 的值 —— RHS 查询

理解比喻

  • LHS:找一个“书架”放书。
  • RHS:找一本具体的“书”。

四、作用域嵌套与查找规则

JavaScript 的作用域是分层嵌套的结构,形成所谓的“作用域链”(Scope Chain)。

查找变量时,遵循以下规则:

  1. 从当前作用域开始查找;
  2. 若未找到,逐层向外查找;
  3. 若查找到全局作用域仍未找到,则抛出错误。

具体情况:

  • LHS 查询

    • 非严格模式:自动创建全局变量;
    • 严格模式:抛出 ReferenceError
  • RHS 查询
    无论是否严格模式,均抛出 ReferenceError


五、常见错误类型

错误类型触发条件
ReferenceErrorRHS 查询失败(变量未定义)
TypeError变量存在,但操作非法(如对 undefined 调用函数)

六、第二章:词法作用域(Lexical Scope)

JavaScript 的作用域模型主要有两种:

  • 词法作用域(Lexical Scope)
  • 动态作用域(Dynamic Scope)

JavaScript 实际采用的是 词法作用域


1. 什么是词法作用域?

词法作用域指:作用域由代码书写时的位置决定,在编译阶段就已确定。
与之相对的动态作用域,是在运行时根据调用位置决定的。

除了 evalwith,JavaScript 作用域基本都是词法作用域。


2. 词法作用域的两条规则

规则一:定义时决定作用域

函数或块的作用域环境在声明时就已经确定,不会被运行时改变。

规则二:运行时无法修改词法作用域
  • eval(str) 可以在运行时生成变量;
  • with(obj) 可以临时扩展作用域链。

但这两者都会破坏作用域的静态性与性能。

建议:避免使用 evalwith


3. “欺骗”词法作用域的代价

eval(str)
function foo(str) {
    eval(str);
    console.log(b);
}

foo("var b = 3;"); // 输出 3

问题:引擎无法在编译阶段优化作用域,运行时性能下降。

with(obj)
with (obj) {
    a = 2; // 可能修改 obj.a,也可能修改全局 a
}

问题:变量归属模糊,调试困难,已被废弃。


4. 作用域气泡模型(Scope Bubbles)

每个作用域可视为一个“气泡”:

  • 全局作用域是最外层气泡;
  • 每个函数创建新的气泡;
  • ES6 的 letconst 会创建块级作用域。
function foo() {
    var a = 1;
    if (true) {
        let b = 2; // 块级作用域
    }
    console.log(a); // 1
    console.log(b); // ReferenceError
}

5. 作用域链与性能优化

作用域链越深,变量查找速度越慢(差距虽小,但确实存在)。

优化建议

  • 减少全局变量;
  • 将常用变量放在当前作用域;
  • 使用 IIFE(立即执行函数表达式)创建私有作用域。

七、第一章与第二章的关联

概念说明
编译三步走分词 → 解析 → 代码生成
LHS vs RHS赋值目标 vs 获取值
作用域链查找从内到外逐层查找
词法作用域写代码时决定作用域
欺骗词法作用域eval / with 会破坏可预测性

八、核心总结

  • JavaScript 在执行前会经历编译阶段。
  • 作用域是一套规则,决定变量的归属与访问权限。
  • 理解 LHS 和 RHS 是掌握作用域机制的关键。
  • 词法作用域在代码书写阶段就已确定。
  • 避免使用 evalwith,它们会影响性能和可维护性。

九、学习建议

  1. 多写代码验证 LHS / RHS 的行为。
  2. 使用浏览器开发者工具查看作用域链。
  3. 尝试从“编译器视角”理解变量声明与查找。
  4. 通过绘制函数嵌套结构理解词法作用域的静态特性。