1.作用域是什么
1.1 编译原理
在传统编译语言的流程中,程序中的一段代码会在执行前经历三个步骤,统称为
编译
- 分词/词法分析(Tokenizing/Lexing)
- 将字符组成的字符串分解成有意义的代码块,这些代码块被称为词法单元(token)
1 | var a = 2; |
- 解析/语法分析(Parsing)
- 将词法单元流(数组)转换成一个由元素逐级嵌套所组成的代表了程序语法结构的树.
抽象语法树(Abstract Syntax Tree, AST)
- 将词法单元流(数组)转换成一个由元素逐级嵌套所组成的代表了程序语法结构的树.
- 代码生成
- 将 AST 转换为可执行代码的过程称为代码生成.
1 | 将var a = 2; 的AST转换为一组机器指令, 用来创建一个叫作a的变量(包括分配内存等),并将一个值2储存在a中 |
但是 JavaScript 引擎要复杂得多,其在语法分析和代码生成阶段有特定的步骤对运行性能进行优化. JavaScript 的编译过程不是发生在构建之前. 大部分情况下编译发生在代码执行的前几微秒的时间内.
任何 JavaScript 代码片段在执行前都要进行编译(通常就在执行前)
1.2 理解作用域
- 引擎
负责整个 JS 程序的编译及执行过程 - 编译器
负责语法分析和代码生成等 - 作用域
负责收集并维护由所有声明的标识符(变量)组成的一系列查询,并实施严格规则,确定当前执行的代码对这些标识符的访问权限
变量的赋值操作会执行两个操作
- 首先编译器会在当前作用域声明一个变量(如果没有声明过)
- 然后在运行时引擎会在作用域中
查找
(由作用域协助
)该变量,如果找到就会对它赋值
LHS
和RHS
查询
LHS
: 赋值操作的目标是谁RHS
: 谁是赋值操作的源头当变量出现在赋值操作的左侧时,进行 LHS 查询;
试图找到变量的容器本身,从而对其赋值
- 出现在右侧进行 RHS 查询;
可以理解为简单的查找某个变量的值,等到某某的值, retrieve his source value(取到它的源值)
1 | function foo(a) { |
1 | // LHS 3处 |
1.3 作用域嵌套
- 作用域是根据名称查找变量的一套规则,实际中通常需要同时兼顾几个作用域
- 当一个快或函数嵌套在另一个块或函数中时,就发生了作用域嵌套. 所以, 在当前作用域中无法找到某个变量时, 引擎就会在外层嵌套的作用域中继续查找,直到找到该变量, 或抵达最外层的作用域(全局作用域)为止
遍历嵌套作用域链的规则
- 引擎从当前的执行作用域开始查找变量,如果找不到,就向上一级继续查找. 当抵达最外层全局作用域时,无论找到与否, 查找过程都会停止
1.4 异常
- 在变量没有声明(任何作用域中都无法找到该变量)的情况下, LHS 和 RHS 查询的行为不一样
1 | function foo(a) { |
当我们第一次对 b 进行 RHS 查询时, 无法找到该变量 , 也就是 b 是一个未声明的变量
- 如果 RHS 查询在所有嵌套的作用域中找不到该变量, 引擎就会抛出 ReferenceError
- 当引擎进行 LHS 查询时, 非严格模式下, 如果在顶层(全局作用域)中也无法找到目标变量,全局作用域中就会创建一个具有该名称的变量,并返还给引擎
- 严格模式禁止自动或隐式地创建全局变量
1.5 总结
- 作用域是一套规则,用于确定在何处以及如何查找变量.如果查找的目的是对变量赋值,那么就会进行 LHS 查询; 如果目的是获取变量的值, 就会使用 RHS 查询.
- 赋值操作符会导致 LHS 查询. 隐式传参也会导致 LHS 查询 foo(2)==>foo(a)
- 不成功的 RHS 引用会导致抛出 ReferenceError 异常, 不成功的 LHS 引用会导致自动隐式创建一个全局变量(非严格模式下)
2. 词法作用域
作用域主要有两种工作模型 词法作用域
动态作用域
(少,bash,Perl)
1 | function foo(){ |