第一章 作用域是什么

Thursday, August 19, 2021

一套设计良好的规则来存储变量,并且之后可以方便地找到这些变量,这套规则被称为作用域。

JavaScript运行过程

JavaScript 在处理代码时, 会与以下三个角色进行协同工作:

  • 引擎 从头到尾负责整个 JavaScript 程序的编译及执行过程。
  • 编译器 负责语法分析及代码生成等脏活累活。
    • 分词/词法分析(Tokenizing/Lexing),这个过程会将由字符组成的字符串分解成(对编程语言来说)有意义的代码块,这些代 码块被称为词法单元(token)。 var a = 2这段程序通常会被分解成为下面这些词法单元:var、a、=、2 、;
    • 解析/语法分析(Parsing),这个过程是将词法单元流(数组)转换成一个由元素逐级嵌套所组成的代表了程序语法 结构的树。这个树被称为“抽象语法树”(Abstract Syntax Tree,AST)。
    • 代码生成,将AST 转换为可执行代码的过程称被称为代码生成。抛开具体细节,简单来说就是有某种方法可以将 var a = 2; 的AST 转化为一组机器指 令,用来创建一个叫作 a 的变量(包括分配内存等),并将一个值储存在 a 中。
  • 作用域 负责收集并维护由所有声明的标识符(变量)组成的一系列查询,并实施一套非常严格的规则,确定当前执行的代码对这些标识符的访问权限。

引擎与编译器

var a = 2

从我们看来,这是一段声明,但引擎会认为这是两个不同的声明(声明与赋值)

var a由编译器在编译时处理,a=2则由引擎在运行(执行)时处理

  • 编译器首先会将这段程序分解成词法单元,然后将词法单元解析成一个树结构(AST)。
  • 当编译器开始进行代码生成时,它对这段程序的处理方式会和预期的有所不同。
    • 遇到 var a, 编译器会询问作用域是否已经有一个该名称的变量存在于同一个作用域的集合中, 有, 则忽略声明, 继续编译, 没有, 则声明一个新的变量。
    • 接下来编译器会为引擎生成运行时所需的代码,这些代码被用来处理 a = 2 这个赋值操作。引擎运行时会询问作用域,在当前的作用域中是否存在 a 变量。如果存在则用于a=2该赋值操作,否则将抛出异常。

引擎与作用域

编译器在编译过程的第二步中生成了代码,引擎执行它时,会通过查找变量 a 来判断它是 否已声明过,查找的过程由作用域进行协助。

如果目的是对变量进行赋值, 引擎使用 LHS 查询; 如果目的是获取变量的值, 引擎使用 RHS 查询。

如, var a = 2, var a 会在编译过程中声明, a = 2 是赋值操作, 我们需要为 = 2 找到一个目标, 所以使用了 LHS 查询。

如, console.log(a), 在这里我们向作用域询问 a 的值, 所以使用了 RHS 查询。

作用域嵌套

当一个块或函数嵌套在另一个块或函数中时,就发生了作用域的嵌套。

function foo(a) { 
    console.log( a + b );
}
var b = 2;
foo( 2 ); // 4
  • foo(2) 传参的操作, 实际上为参数 a 进行了隐式的赋值操作 a = 2
  • 变量 a 存在于 foo 的函数作用域中, 外部无法访问
  • foo 函数中引用了 b 变量, 引擎会先尝试在 foo 函数作用域中查找(RHS)该变量, 如果找不到, 就会往上一级作用域查找, 以此类推, 直到找到为止, 而这里的 b 变量, 存在于 全局作用域 中.

根据以上示例, 我们可以把作用域比作一个建筑, 这个建筑代表程序中的嵌套作用域链, 第一层代表当前的执行作用域, 建筑的顶层代表全局作用域。

LHS 和 RHS 引用都会在当前楼层进行查找, 如果没找到, 就会坐电梯上一层, 以此类推, 最后到达顶楼(全局作用域), 无论程序是否已经找到你所需的变量, 都为到此为止。

异常

为什么区分 LHS 和RHS 是一件重要的事情?

因为在变量还没有声明(在任何作用域中都无法找到该变量)的情况下,这两种查询的行 为是不一样的。

RHS未查询到变量,会抛出ReferenceError错误,LHS未查询到变量,全局作用域中就会创建一个具有该名称的变量**(非严格模式下)**

console.log(a) // ReferenceError: a is not defined

b = 2 // 全局作用域中创建b变量,并将其返还给引擎,引擎再对b进行赋值
console.log(b) // 2

假设RHS查询到变量,但对该变量的值进行不合理操作时,则会抛出TypeError错误

// 引用 null 或 undefined 类型值中的属性
console.log(null.a) // TypeError: Cannot read property 'a' of null

// 对一个非函数类型的值进行函数调用
const b = 'str'
b() // TypeError: b is not a function