作为《你不知道的Javascript》忠实读者,多次拜读该著作,本专栏用来分享我对该书的解读,适合希望深入了解这本书的读者阅读 电子书下载网址:zh.101-c.online
第二章——词法作用域
本章在《你不知道的Javascript(上卷)》中并没用较大的篇幅去描述,书中所讲主要聚焦在定义阶段,以及欺骗词法,我们的解读将由“就是这样”向“为什么是这样”转变。下面我们去讨论一些问题。
一、为何是词法作用域?
原文表述
Javascript便是使用的词法作用域而非动态作用域,那么什么是词法作用域,什么是动态作用域呢?(这里做一个总结,有书中内容,也有总结内容)
词法作用域(Lexical Scope)
定义:词法作用域(也称为静态作用域)是由代码的书写结构决定的,在代码编译阶段(词法分析时)就确定了变量的作用域,不会在运行时改变。
特点:
- 由代码结构决定:作用域在代码编写时就固定,函数的作用域取决于它被定义的位置,而不是调用的位置。
- JavaScript 采用词法作用域:JavaScript 的作用域规则是词法作用域。
- 闭包的基础:由于词法作用域的存在,函数可以访问定义时的外层变量,即使在外层函数执行完毕后仍然有效(闭包)。
示例:
var a = 10;
function foo() {
console.log(a); // 10(查找 foo 定义时的作用域,而非调用时的作用域)
}
function bar() {
var a = 20;
foo(); // 仍然输出 10,因为 foo 的词法作用域在全局
}
bar();
由于
foo
在全局定义,它的作用域链在编译时就确定了,即使bar
内部有同名变量a
,foo
仍然访问全局的a
。
动态作用域(Dynamic Scope)
定义:动态作用域是在运行时根据调用栈决定的,函数的作用域取决于它被调用的位置,而不是定义的位置。
特点:
- 由调用链决定:变量的查找基于函数的调用顺序,而不是代码结构。
- JavaScript 默认不支持动态作用域,但
this
的绑定机制(如call
、apply
、bind
)有些类似动态作用域的行为。 - Bash、Perl 等语言支持动态作用域。
示例(假设 JavaScript 是动态作用域):
var a = 10;
function foo() {
console.log(a); // 如果是动态作用域,这里会输出 20
}
function bar() {
var a = 20;
foo(); // 动态作用域下,foo 会查找 bar 的 a
}
bar();
如果 JavaScript 是动态作用域,
foo
会查找调用它的bar
的作用域,输出20
。但实际 JavaScript 是词法作用域,仍然输出10
。
那么之后我们就进入本篇文章的第一个要点 “为什么Javascript要使用词法作用域,而不使用动态作用域?”,想必各位也能想出其中一二,比如动态作用域会使语言的作用域不清晰,不便于开发;动态作用域使代码性能下降等,这里我们做一个系统的分析和总结
1. 可预测性与代码可维护性
- 词法作用域 在代码 编写时 就确定了变量的作用域,开发者能清晰知道一个变量来自哪里(例如函数定义时的外层作用域)。
- 动态作用域 的变量查找依赖运行时调用链,导致代码行为难以预测,尤其是大型项目或嵌套调用时。
示例对比:
// 词法作用域(可预测)
var x = 10;
function foo() { console.log(x); }
function bar() { var x = 20; foo(); }
bar(); // 输出 10(foo 始终访问定义时的 x)
// 如果是动态作用域(不可预测)
bar(); // 会输出 20(foo 访问调用时的 bar 的 x)
动态作用域下,
foo
的输出会因调用位置不同而变化,增加调试难度。
2. 性能优化
- 词法作用域 在编译阶段即可确定变量引用,引擎可以优化作用域链的查找(如静态分析、内联缓存)。
- 动态作用域 需要在运行时动态解析变量,每次调用都可能重新计算作用域链,显著降低性能。
底层优化:
V8 引擎通过 隐藏类(Hidden Classes) 和 内联缓存(Inline Caches) 加速词法作用域的变量访问,而动态作用域无法应用这些优化。
3. 闭包的支持
- 词法作用域是闭包的基础:函数可以记住并访问定义时的作用域,即使在外层函数执行完毕后仍然有效。
- 动态作用域无法实现闭包,因为变量的绑定在运行时才确定。
闭包示例:
function outer() {
var x = 10;
function inner() { console.log(x); } // 记住 outer 的 x
return inner;
}
var fn = outer();
fn(); // 输出 10(词法作用域允许闭包)
如果 JavaScript 是动态作用域,
inner
无法可靠访问x
,因为它的值取决于调用时的上下文。
4. 模块化的天然支持
- 词法作用域 允许通过函数嵌套和闭包实现模块化(如 IIFE 模式)。
- 动态作用域 的变量容易受外部调用污染,难以隔离作用域。
模块化示例:
// 模块模式(依赖词法作用域)
var module = (function() {
var privateVar = 1;
return { get: function() { return privateVar; } };
})();
module.get(); // 1(privateVar 被保护)
动态作用域下,
privateVar
可能被外部代码意外修改。
5. 历史与语言设计哲学
- JavaScript 受 Scheme/Lisp 影响:Brendan Eich 在设计 JavaScript 时借鉴了 Scheme 的词法作用域特性,强调函数式编程的简洁性。
- 动态作用域更适合脚本语言:如 Bash、Perl,它们需要频繁依赖运行时上下文,但牺牲了可维护性。
至此,我们的第一部分完结
二、欺骗词法的性能问题
书中欺骗词法的讲述较为清晰,这里我只提一下 “欺骗词法的性能问题”
先来看一下原文表述
原文的表述主要聚焦在欺骗词法破坏了代码词法的静态分析,在一定程度上出现了“动态作用域的性能问题”,经管Javascript是词法作用域,但是欺骗词法是词法作用域的优势不再。那有没有更多的角度去分析呢?——
JavaScript 引擎(如 V8)在编译阶段会进行 静态作用域分析,优化变量访问,而 eval
、with
等动态作用域操作会破坏这种优化,导致性能下降。具体原因如下:
1. 破坏作用域静态分析
(1) 词法作用域的优化机制
-
编译阶段:引擎在代码执行前就能确定变量属于哪个作用域,生成高效的字节码或机器码。
function foo() { var a = 1; console.log(a); // 引擎知道 a 是局部变量,直接访问栈内存 }
-
优化手段:
- 内联缓存(Inline Cache):缓存变量位置,避免重复查找。
- 隐藏类(Hidden Class):快速定位对象属性。
(2) 动态作用域的破坏
eval
或 with
会让引擎无法在编译阶段确定变量来源,必须 运行时动态解析:
function riskyEval(str) {
eval(str); // 可能插入新变量,如 eval("var b = 2;")
console.log(b); // 引擎无法提前知道 b 是否存在
}
riskyEval("var b = 3;");
后果:
- 引擎无法预分配变量存储位置(栈/堆)。
- 所有变量访问退化为 慢速的动态查找(类似哈希表查询)。
2. 禁用 JIT 优化
(1) 去优化(Deoptimization)
现代 JavaScript 引擎(如 V8)会先快速生成未优化的字节码,运行中如果发现热点代码(频繁执行),则用 JIT(Just-In-Time)编译 生成优化后的机器码。
动态作用域操作会触发去优化:
function unstable(x) {
with ({ x: 1 }) {
return x; // 引擎无法静态确定 x 的来源
}
}
// 首次执行:生成未优化代码
unstable(10);
// 后续执行:发现 with 无法优化,回退到解释执行
后果:
- 优化后的机器码被丢弃,回退到慢速的解释执行模式。
- 性能可能下降 10~100 倍。
(2) 无法内联缓存(Inline Cache)
-
正常情况:引擎会缓存对象属性的内存偏移量,加速访问:
obj.a; // 第一次访问记录位置,后续直接跳转
-
动态作用域下:
with (obj) { console.log(a); // 无法缓存,每次都要查找 }
每次访问都需重新计算属性位置,类似
HashMap.get("a")
,速度极慢。
3. 内存与安全开销
(1) 作用域链膨胀
eval
可能意外注入变量,污染作用域:
function leaky() {
eval("var secret = 123;");
// secret 泄漏到函数作用域,可能被闭包长期持有
}
后果:
- 变量无法被及时垃圾回收,增加内存占用。
- 作用域链变长,变量查找时间增长。
(2) 安全风险
动态代码执行(如 eval
)可能引发 XSS 攻击或意外行为,引擎会启用额外的安全检查,进一步拖慢速度。
结语
JavaScript 的词法作用域设计体现了语言在灵活性与性能之间的精妙平衡。通过静态作用域规则,JavaScript 既保证了代码的可预测性和可维护性,又为引擎优化提供了坚实基础。 这种设计让开发者能够构建复杂的模块化系统,同时享受现代 JIT 编译器带来的极致性能。词法作用域不仅是 JavaScript 的核心特性,更是理解闭包、this 绑定等高级概念的关键入口。
然而,这种优雅的设计也划定了明确的边界。任何试图"欺骗"词法作用域的操作(如 eval 和 with)都会破坏引擎的静态分析能力,导致严重的性能惩罚。 这提醒我们:在追求动态灵活性的同时,必须尊重语言的核心设计哲学。理解这些底层机制,不仅能帮助我们写出更高效的代码,更能深入体会 JavaScript 作为一门精心设计的语言所蕴含的智慧。
第二章的内容还是太少了,我更期待第三章的内容......