这是我阅读《You Don’t Know JavaScript》的过程中,记录下来的读书笔记。
一、作用域是什么?
-
作用域是一套设计良好的规则,用来存储变量,并且之后可以方便的找到这些变量。
-
几乎是所有编程语言最基本的功能之一,就是能够存储变量当中的值,并且能在之后对这个值进行访问或修改。事实上,正是这种存储和访问变量的值的能力,将状态带给程序。
-
那么变量存储在哪?程序如何找到它们?
二、编译原理
- JavaScript实际上是一门编译语言。在传统编译语言的流程中,程序中的一段源代码在执行之前会经历三个步骤,统称为"编译"。
1. 分词/词法分析(Tokening/Lexing)
- 这个过程会将由字符组成的字符串分解成有意义(对编程语言来说)的代码块,这些代码块被成为词法单元(token)。
var a = 2;
分解为:var、a、=、2、;
2. 解析/语法分析(Parsing)
- 这个过程是将词汇单元流(数组)转换成一个由元素逐级嵌套所组成的代表了程序语法结构的树。这个树被称为“抽象语法树”。
3. 代码生成
- 将AST转换为可执行的代码的过程被成为代码生成。这个过程与语言、目标平台等息息相关。
- 简单来说,就是将var = 2;的AST转化为一组机器指令,用来创建一个叫做a的变量(包括分配内存等),并将一个值存储在a中。
4. 准备执行
- 比起哪些编译只有三个步骤的语言的编译器,JavaScript引擎要复杂得多。例如,在语法分析和代码生成阶段有特定的步骤来对运行性能进行优化,包括对冗余元素进行优化等。
- 对于JavaScript来说,大部分情况下编译发生在代码执行前的几微秒(甚至更短)的时间内。JavaScript引擎用尽各种办法(如JIT,可以延迟编译)来保证性能最佳。
- 也就是说,经过代码前三个步骤之后,就做好执行它的准备,并通常马上就会执行它。
三、理解作用域
先简单了解代码执行过程
1. 三个角色
- 引擎:从头到尾负责整个JavaScript程序的编译及执行过程。
- 编译器:负责语法分析和代码生成等。(前面介绍过)
- 作用域:负责收集并维护由所有声明的标识符(变量)组成的一系列查询,并实施一套非常严格的规则,确定当前执行的代码对这些标识符的访问权限。
2. 大致过程
- 以var = a;为例,变量的赋值操作会执行两个动作
- 首先编译器会在当前作用域中声明一个变量(如果之前没有声明过)
- 然后在运行时引擎会在作用域中查找该变量,如果能够找到就会对它赋值。
3. 查询
- 分为
LHS查询
和`RHS查询`` - ``LHS查询`:试图找到变量的容器本身(用于赋值)
a = 2 // 对a进行LHS查询
RHS查询
:查找某个变量的值(获取值)
console.log(a) // 对a进行RHS查询
4. 作用域的作用
- 在引擎执行代码的过程中,需要对变量进行LHS查询和RHS查询,这个过程就需要作用域进行查找。
四、作用域嵌套
实际代码中,可能需要顾及几个作用域。
1. 变量查找
- 当一个块或函数嵌套在另一个块或函数中时,就发生了作用域嵌套。
- 在当前的作用域内无法找到某个变量时,引擎就会在外层嵌套的作用域中继续查找,直到找到该变量,或抵达最外层的作用域为止。
2. 举例说明
function foo(a) {
console.log(a + b);
}
var b = 2;
foo(2); // 4
- 这里对a进行RHS查询可以在函数foo内部完成,但是b无法完成,需要到上一级作用域中完成(在这个例子中是全局作用域)。
- 也就是引擎会先在foo作用域内对变量b进行RHS引用,如果失败,引擎就在foo的上一级作用域对变量b进行RHS引用。
- 嵌套作用域的规则:引擎从当前的执行作用域开始查找变量,如果找不到,就向上一级作用域查找。这个过程中,如果找到了,就会停止查找。当抵达最外层的全局作用域时,无论找到还是没找到,查找过程都会停止。
3. 图片说明
嵌套作用域比作一栋楼,变量查找就是在当前的位置开始,向上一层一层查找。直到顶层的全局作用域。
五、词法作用域
1. 介绍
- 作用域有两种主要的工作模型。第一种是最为普遍的,被大多数编译语言所采用的词法作用域。另外一种叫做动态作用域(Bash脚本。Perl等)。
- 词法作用域是由你在写代码时函数声明的位置来决定的。
2. 作用
- 编译的词法分析阶段基本能够知道全部标识符在哪里以及是如何声明的。
- JavaScript引擎在编译阶段,能够根据代码的词法进行静态分析,并预先确定所有变量和函数的定义位置,才能在执行过程中快速找到标识符。
- 第一,包含着整个全局作用域,其中只有一个标识符:foo。
- 第二,包含着foo所创建的作用域,其中有三个标识符:a、bar和b。
- 第三,包含着bar所创建的作用域,其中只有一个标识符:c。
3. 查找
- 比如上面例子中的a,是从bar的作用域中开始查找标识符,没有找到就往上一级foo创建的作用域,这个时候找到了就停止查找。
- 遮蔽:如果存在同名的标识符,内部会遮蔽外部的。
4. 修改词法
修改词法,也可以说欺骗词法。
eval()
- 可以接受一个字符串为参数,并将其中的内容视为好像在书写时就存在与程序中这个位置的代码。
- 运行时有自己的词法作用域,意味着其中的声明无法修改所在的作用域。
with
- 通常被当作重复引用同一个对象中的多个属性的快捷方式。
- 可以将一个没有或有多个属性的对象处理为一个完全隔离的词法作用域。
问题:
- JavaScript引擎会在编译阶段进行数项的性能优化。其中有些优化依赖于能够根据代码的词法进行金泰分析。修改词法会导致这些优化没有意义。
六、总结
基本作用
-
作用域是用来保存变量和方便快速查找变量的。
-
JS引擎执行代码过程中,会在作用域进行标识符查找,用于变量赋值或取值。
查找过程
- JS引擎从代码执行位置所在作用域开始查找,如果当前作用域没有查找到该标识符,就到查找上一级作用域,一直到找到或到全局作用域为止。
- 这里注意同名的标识符,内部会遮蔽外部的。
词法作用域
-
词法作用域是由你在写代码时函数声明的位置来决定的。
-
JavaScript引擎在编译阶段,能够根据代码的词法进行静态分析,并预先确定所有变量和函数的定义位置,才能在执行过程中快速找到标识符。