在前端开发的学习旅程中,JavaScript 的作用域和词法作用域是基石般的存在,却也常常被开发者一知半解。近期学习了《你不知道的 JavaScript》前两章,对这部分知识有了更系统的认知,特此整理学习笔记,与大家分享。
第 1 章 作用域是什么
1.1 编译原理
JavaScript 虽然是一门解释型语言,但它的执行也离不开 “编译” 环节,只是这个过程非常迅速且隐蔽。传统编译语言的编译过程分为分词 / 词法分析、解析 / 语法分析、代码生成三个阶段,JavaScript 的编译过程与之类似。
- 分词 / 词法分析:将代码中的字符序列分解成有意义的词法单元,比如
var a = 2;会被分解为var、a、=、2、;这些词法单元。 - 解析 / 语法分析:将词法单元转换为抽象语法树(AST),这是一种对代码结构的抽象表示。
- 代码生成:将抽象语法树转换为可执行的机器码。
理解这一过程,能帮助我们从底层视角看待变量的声明、赋值等操作。
1.2 理解作用域
作者用了非常形象的类比来帮助读者理解作用域。
-
演员表:将变量、函数等标识符类比为演员,它们在代码中扮演特定的角色。
-
对话:代码的执行过程就像一场对话,引擎(负责执行代码)、编译器(负责词法分析、编译等)和作用域(负责管理标识符的查找)三者之间不断交流。
- 当遇到
var a = 2;时,编译器首先会询问作用域:“是否已经有一个叫做a的变量在当前作用域中?” 如果没有,编译器会要求作用域声明一个新的变量a,然后编译器会生成代码,让引擎在运行时将2赋值给a。
- 当遇到
-
编译器有话说:编译器在编译阶段会对变量进行声明处理,这也就解释了 JavaScript 中的 “变量提升” 现象,即变量的声明会被提升到作用域的顶部,而赋值操作则保留在原地。
-
引擎和作用域的对话:当引擎执行代码时,遇到变量引用,会向作用域查询该变量的值。如果当前作用域没有,就会向父级作用域查询,直到全局作用域,这就是作用域的查找机制。
-
小测验:通过一些示例题目,比如
a = 2; var a; console.log(a);,来验证对变量提升的理解,答案是2,因为var a的声明被提升,而a = 2的赋值在运行时执行。
1.3 作用域嵌套
在 JavaScript 中,作用域是可以嵌套的,就像俄罗斯套娃。如果一个作用域嵌套在另一个作用域中,那么内部作用域可以访问外部作用域的变量,反之则不行。
例如:
javascript
var a = 1;
function outer() {
var b = 2;
function inner() {
var c = 3;
console.log(a, b, c); // 1, 2, 3
}
inner();
console.log(a, b); // 1, 2
}
outer();
console.log(a); // 1
在inner函数作用域中,可以访问outer作用域的b和全局作用域的a。
1.4 异常
当引擎在作用域链中查找一个变量,直到全局作用域都没有找到时,就会抛出ReferenceError异常;而如果找到了变量,但对其进行了不合理的操作,比如对一个非函数变量进行函数调用,就会抛出TypeError异常。
1.5 小结
第 1 章从宏观角度介绍了作用域的概念,通过编译原理、形象类比等方式,让我们理解了作用域是如何管理标识符的声明和查找的,为后续学习词法作用域奠定了基础。
第 2 章 词法作用域
2.1 词法阶段
词法作用域是由代码的书写位置决定的,在词法分析阶段就已经确定了,因此也称为静态作用域。
换句话说,当我们编写代码时,每个函数被定义的位置就决定了它的词法作用域,也就是它能访问哪些变量。
例如:
javascript
var a = 10;
function foo() {
var b = 20;
function bar() {
var c = 30;
console.log(a, b, c); // 10, 20, 30
}
bar();
}
foo();
bar函数的词法作用域由它在代码中被定义的位置决定,它可以访问foo作用域的b和全局作用域的a。
2.2 欺骗词法
虽然词法作用域是静态的,但 JavaScript 中有一些机制可以 “欺骗” 词法作用域,不过这些机制并不推荐使用,因为会破坏代码的可读性和性能。
-
eval:
eval函数可以在运行时动态执行一段代码,并且这段代码会被视为在当前作用域中书写的,从而修改词法作用域。例如:
javascript
var a = 1; function foo() { eval("var a = 2;"); console.log(a); // 2 } foo(); console.log(a); // 1在
foo函数中,eval执行的代码var a = 2;被视为在foo作用域中书写的,因此修改了foo作用域中的a,而全局作用域的a不受影响。 -
with:
with语句用于将一个对象的属性视为当前作用域中的变量。例如:
javascript
var obj = {a: 1, b: 2}; with(obj) { a = 3; b = 4; c = 5; } console.log(obj.a, obj.b); // 3, 4 console.log(c); // 5(在全局作用域中)with将obj的属性a、b视为当前作用域的变量,对c的赋值因为在obj中找不到,所以被赋值到了全局作用域。 -
性能:使用
eval和with会导致 JavaScript 引擎无法在编译阶段对作用域进行优化,因为它们会动态改变词法作用域,使得引擎无法确定变量的查找位置,从而降低代码的执行性能。
2.3 小结
第 2 章详细介绍了词法作用域,它是 JavaScript 作用域的核心机制,由代码的书写位置决定。同时也了解了eval和with这两个可以 “欺骗” 词法作用域的机制,以及它们对性能的负面影响,在实际开发中应尽量避免使用。
个人总结
通过对这两章的学习,我对 JavaScript 的作用域有了更深入、更系统的理解,这对于后续学习闭包、模块化等知识至关重要。在日常开发中,清晰的作用域认知能帮助我们避免变量污染、理解代码的执行流程,从而写出更健壮、更高效的代码。