(上一)1-作用域是什么

什么是作用域

几乎所有编程语言最基本的功能之一,就是能够储存变量当中的值,并且能在之后对这个值进行访问或修改。

作用域就是一套设计良好的规则。用于管理引擎确定在何处以及如何查找变量(标识符)。如果查找的目的是对
变量进行赋值,那么就会使用 LHS 查询;如果目的是获取变量的值,就会使用 RHS 查询。赋值操作符会导致 LHS 查询。=操作符或调用函数时传入参数的操作都会导致关联作用域 的赋值操作。

1.1 编译原理

尽管通常将 JavaScript 归类为“动态”或“解释执行”语言,但事实上它是一门编译语言。在传统编译语言的流程中,程序中的一段源代码在执行之前会经历三个步骤,统称为“编译”。

  • 分词/词法分析(Tokenizing/Lexing) —— 这个过程会将由字符组成的字符串分解成(对编程语言来说)有意义的代码块,这些代 码块被称为词法单元(token)
  • 解析/语法分析(Parsing) —— 这个过程是将词法单元流(数组)转换成一个由元素逐级嵌套所组成的代表了程序语法 结构的树。这个树被称为“抽象语法树”(Abstract Syntax Tree,AST)
  • 代码生成 —— 将 AST 转换为可执行代码的过程称被称为代码生成。这个过程与语言、目标平台等息 息相关

简单来讲整个过程。以 var a = 2;(以下所有解释都以此为例),但实际上,JavaScript 引擎要复杂得多。例如,在 语法分析和代码生成阶段有特定的步骤来对运行性能进行优化,包括对冗余元素进行优化 等

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var a=2

// 1. 首先这段程序会被分解成 为下面这些词法单元:`var、a、=、2`
['var', ' ', 'a', '=', '1']

// 2. 第二步会将这些词法单元流转换为一个描述该语法的抽象语法树(AST)
{
VariableDeclaration: {
Identifier: 'a',
AssignmentExpression: {
NumericLiteral: 2
}
}
}

// 3. 第三步将 AST 转化为一组机器指令,用来创建一个叫作 a 的变量(包括分配内存等),并将一个值储存在 a 中

1.2 理解作用域

要理解 JavaScript 的工作原理,首先我们要认识这几位主角:

  • 引擎 —— 从头到尾负责整个 JavaScript 程序的编译及执行过程。
  • 编译器 —— 引擎的好朋友之一,负责语法分析及代码生成等脏活累活
  • 作用域 —— 引擎的另一位好朋友,负责收集并维护由所有声明的标识符(变量)组成的一系列查询,并实施一套非常严格的规则,确定当前执行的代码对这些标识符的访问权限

变量的赋值操作会执行两个动作,以var a = 2为例:

  1. 首先编译器会在当前作用域中声明一个变量(如果之前没有声明过),即 var a 在其作用域中声明新变量。这会在最开始的阶段,也就是代码执行前进行
  2. 接下来编译器会为引擎生成运行时所需的代码。然后在运行时引擎会在作用域中查找该变量(查找的过程由作用域进行协助),如果能够找到就会对它赋值。即 a = 2 会查询(LHS 查询)变量 a 并对其进行赋值

其中,在第二步引擎在查找变量时主要会通过两种方式:

  • LHS —— 当变量出现在赋值操作的左侧时进行 LHS 查询,LHS 查询则是试图 找到变量的容器本身,从而可以对其赋值
  • RHS —— 当变量出现在右侧时进行 RHS 查询,RHS 查询与简单地查找某个变量的值别无二致

1.3 作用域嵌套

LHS 和 RHS 查询都会在当前执行作用域中开始,如果有需要(也就是说它们没有找到所 需的标识符),就会向上级作用域继续查找目标标识符,这样每次上升一级作用域(一层 楼),最后抵达全局作用域(顶层),无论找到或没找到都将停止。

不成功的 RHS 引用会导致抛出 ReferenceError 异常。不成功的 LHS 引用会导致自动隐式 地创建一个全局变量(非严格模式下),该变量使用 LHS 引用的目标作为标识符,或者抛 出 ReferenceError 异常(严格模式下)。

总结

1. 作用域是什么?

作用域就是一套设计良好的规则。用于管理引擎确定在何处以及如何查找变量(标识符)。如果查找的目的是对变量进行赋值,那么就会使用 LHS 查询;如果目的是获取变量的值,就会使用 RHS 查询

在实现机制上,作用域就是负责收集并维护由所有声明的标识符(变量)组成的一系列查询,并实施一套非常严格的规则,确定当前执行的代码对这些标识符的访问权限

2. 作用域嵌套是什么?

当一个块或函数嵌套在另一个块或函数中时,就发生了作用域的嵌套。引擎从当前的执行作用域开始查找变量,如果找不到,就向上一级继续查找。当抵达最外层的全局作用域时,无论找到还是没找到,查找过程都会停止

3. 编译器,引擎,作用域之间是怎么合作的?

Javascript 是一门编译语言。源程序在执行前会经历下面三个步骤:分词/词法分析、解析/语法分析、代码生成。由编译器完成。接下来再看下面例子

var a = 2 为例,编译器首先会将这段程序分解成词法单元,然后将词法单元解析成一个树结构(AST),最后当编译器开始进行代码生成时,会进行下面步骤:

  1. 遇到var a编译器会询问作用域是否已经有一个该名称的变量存在于同一个作用域的集合中。如果是,编译器会忽略该声明,继续进行编译;否则它会要求作用域在当前作用域的集合中声明一个新的变量,并命名为 a;
  2. 接下来编译器会为引擎生成运行时所需的代码,这些代码被用来处理 a = 2 这个赋值操作。引擎运行时会首先询问作用域,在当前的作用域集合中是否存在一个叫作 a 的变量。如果是,引擎就会使用这个变量;如果否,引擎会继续查找该变量
0%