一、JavaScript 不是“纯解释型语言”
学习收获
在学习之前,我以为 JS 是解释型脚本语言。
但书中指出:JavaScript 实际上也会在执行前经历“编译阶段”!
核心知识点
-
引擎执行的三大步骤:
- 词法分析(Tokenizing) :将源码拆分为一个个语法单元(token)。
- 语法解析(Parsing) :将 token 组合成一棵语法树(AST)。
- 代码生成(Code Generation) :把 AST 转化为机器可以执行的指令。
-
JavaScript 的编译是 即时完成的(Just-In-Time,JIT) ,在代码执行前的几微秒内完成。
-
因此,像 变量提升、作用域解析 等现象,其实都发生在编译阶段,而非运行时。
二、作用域的三位主角
三大角色
| 角色 | 职责 |
|---|---|
| 引擎(Engine) | 负责代码的执行 |
| 编译器(Compiler) | 在代码运行前进行编译 |
| 作用域(Scope) | 负责收集变量、控制访问权限 |
变量创建过程
var a = 2;
编译阶段:
编译器告诉作用域:“我要声明一个变量 a。”
执行阶段:
引擎在作用域中找到变量
a,并赋值2。
三、LHS 与 RHS 查询
在作用域中查找变量时,存在两种模式:
| 类型 | 查找目的 | 示例 |
|---|---|---|
| LHS(Left-hand Side) | 查找变量容器,用于赋值 | a = 2 |
| RHS(Right-hand Side) | 查找变量的值,用于取值 | console.log(a) |
示例
var a = 2; // LHS 查询:找到 a,用来放入值 2
console.log(a); // RHS 查询:读取 a 的值进行打印
- RHS 查询失败 → 抛出
ReferenceError - LHS 查询失败(非严格模式) → 自动在全局创建变量( 危险)
四、词法作用域(Lexical Scope)
学习收获
JavaScript 的作用域 不是运行时决定的,而是在定义阶段确定的。
核心理解
- JS 使用 词法作用域(Lexical Scope) 。
- 函数在定义时就决定了能访问哪些变量。
- 调用位置不会改变作用域链。
示例
function foo() {
var a = 2;
function bar() {
console.log(a);
}
bar();
}
foo(); // 输出 2
函数
bar在定义时就“记住”了外部的作用域,因此能访问变量a。
这正是 闭包 的理论基础。
五、作用域链与嵌套
每当定义一个函数,都会创建一个新的作用域“气泡”。
内层作用域可以访问外层作用域的变量,查找遵循由内向外、就近优先原则。
function foo() {
var a = 1;
function bar() {
var b = 2;
console.log(a + b);
}
bar();
}
foo(); // 输出 3
六、同名标识符与遮蔽效应(Shadowing)
定义
多层嵌套的作用域中可以定义同名变量,内层变量会遮蔽外层同名变量。
示例
var a = 1;
function foo() {
var a = 2; // 内层同名变量
console.log(a);
}
foo(); // 输出 2
console.log(a); // 输出 1
查找在找到第一个匹配标识符时立即停止。
因此,内层变量会“遮蔽”外层变量(Shadowing)。
延伸例子
var a = "global";
function outer() {
var a = "outer";
function inner() {
var a = "inner";
console.log(a);
}
inner(); // inner
console.log(a); // outer
}
outer();
console.log(a); // global
七、欺骗词法作用域(Cheating Lexical Scope)
JS 的词法作用域在定义时固定,但 eval() 与 with() 可以在运行时篡改作用域。
1. eval() 的作用
eval() 能动态执行字符串形式的代码。
function foo(str) {
eval(str);
console.log(a);
}
foo("var a = 42;"); // 输出 42
在执行时,eval() 会动态向当前作用域注入变量。
-
在严格模式下(
'use strict'),eval()创建独立作用域,不影响外层变量。 -
不论是否严格模式,都会:
- 破坏引擎的静态作用域分析;
- 影响性能;
- 带来安全隐患。
2. with() 的作用
with() 可以临时将对象添加到作用域链前端。
var obj = { a: 1, b: 2 };
with (obj) {
a = 3; // 修改 obj.a
c = 4; // 若 c 不在 obj 中,则创建全局变量 c
}
console.log(obj.a); // 3
console.log(c); // 4
问题:
- 引擎无法确定变量属于对象属性还是外部作用域;
- 编译器无法进行静态分析;
- 严格模式下,
with被完全禁止。
八、欺骗词法作用域导致性能下降的原因
| 原因 | 说明 |
|---|---|
| 无法在编译阶段确定变量绑定关系 | 引擎无法预先建立作用域映射,只能运行时动态查找 |
| 编译优化失效 | JS 引擎优化器无法进行作用域内联或常量折叠 |
| 查找成本上升 | 每次访问变量都要重新判断归属 |
| 破坏词法一致性 | 代码可读性与可预测性下降,调试困难 |
eval()与with()是引擎优化的“天敌”, 在现代开发中应完全避免使用。
通过学习《你不知道的 JavaScript》第一、二章,我获得了对 JavaScript 运行机制更深层的理解:
- JavaScript 并非单纯的解释型语言,而是会先经历编译过程(词法分析 → 语法解析 → 代码生成),这解释了“变量提升”等机制的根本原因。
- 词法作用域是 JS 的灵魂:变量的可访问性在函数定义阶段就已决定,而非运行时动态变化。
- 作用域链与**查找方式(LHS / RHS)**揭示了变量是如何被定位、赋值、取值的。
- 遮蔽效应提醒我们命名规范的重要性,避免变量名冲突带来的混乱。
- eval() 与 with() 是“欺骗词法”的陷阱:它们让引擎无法在编译阶段优化代码,既影响性能,又破坏可维护性。