理解作用域
我们将值存储在变量里面,这些变量储存在哪里?最重要的是 : 当我们要使用的时候,如何找到它?
这些问题说明需要设计一套良好的规则来对这些变量进行存储管理,并且之后可以方便的找到这些变量。而这套规则被称为作用域
所以说,作用域是一套查询变量的规则,说直白一点就是:作用域是规则
那么作用域是规则,怎么体现出规则的?下面先看点别的东西,然后就以一个简单的例子来帮助我们理解作用域。
编译原理
通常一段程序在执行之前都会经历三个步骤,这三个步骤统称为编译。
- 分词/词法解析
在这个阶段会将字符组成的字符串进行分解成为有意义的代码块,这些代码块被称为词法单元,就好比如说 var a = 2;在这个阶段会被分解成为下面这些词法单元:var , a , = , 2 , ; - 解析/语法分析
在这个阶段会将词法解析后的一些词法单元流转换成一个AST树(抽象语法树)。 - 代码生成
将AST树转换成可执行的代码的过程。
上面的三个过程和Vue的模板编译的过程感觉有点相似。
template -> AST -> render()。
参与人员
- 引擎:从头到尾负责javascript程序的编译和执行过程
- 编译器:负责语法分析和代码生成等
- 作用域:负责收集并维护所有的声明标识符(变量)组成的一系列的查询,并实施一套非常严格的规则,确定当前执行的代码对这些标识符的访问权限。
对话
就拿下面这句简单的代码来说,
var = 2;
当执行这段程序之前,编译器会将var=2;这串字符串分解成为词法单,然后组成AST,但在代码生成的阶段,编译器做了下面的处理:
- 遇到var a,编译器会
向作用域询问,是否有一个该名称的变量存在于同一个作用域的集合中,如果是,则会忽略该声明,继续进行编译,否则就会在当前作用域的集合当中声明一个新的变量,并且命名为a。 - 接下来生成运行时所需要的代码,这些代码是被用来处理 a = 2这个赋值操作的,引擎运行的时候,首先会
询问作用域,是都存在一个叫做a的变量,如果是,引擎就会使用这个变量,如果不是,则继续查找(作用域链)。如果找到了,就赋值,没有找到,就抛出一个错误。
LHS和RHS
为了进一步理解,我们需要一些编译器的术语
所谓LHS和RHS就是询问作用域时的类型,LHS就是变量在赋值操作的左边,RHS就是在右边(取值)。
考虑下面代码:
console.log(a);
这里对a进行了一次RHS,因为这里a没有被赋值,相对应的是需要取值,才能传递给console.log()
相比之下,例如:
a = 2;
这里对a进行了一次LHS,因为我们不关心a的值是什么,只想要知道有没有a (为=2找到一个赋值的目标)。 为什么要区分LHS和RHS呢? 因为在变量未声明的情况下,这两种查询是不一样的:
function foo(a){
console.log(a+b);
b = a;
}
foo(2);
第一次对b进行RHS查询的时候是无法找到该变量的,也就是说这是一个未声明的变量,无法在其相关的作用域中找到。如果RHS没找到的话,那么就会抛出ReferenceError异常。
相比之下,当引擎执行LHS查询的时候,如果在相关作用域下没有找到目标变量,那么就会在全局作用域中添加这个变量,并将其返回给引擎(非严格模式)。这就是为什么没有用var 声明的变量会被添加到全局作用域里面的原因!
如果RHS查询找到了这个变量,但是对这个变量进行一些不正当的操作,例如对一个非函数类型的值进行函数调用的时候,那么就会抛出TypeError异常。
作用域嵌套(作用域链)
当一个块或者函数嵌套在另一个块或者函数中的时候,就发生了作用域的嵌套,因此在当前作用域中无法找到某个变量的时候,引擎就会在外层嵌套的作用域中继续查找,知道找到该变量,或者抵达全局作用域为止。
function foo(){
var a =10;
function bar(){
console.log(a);//10
}
}
foo()
在bar函数里面,对a执行的RHS没有找到,那么就去外层foo的作用域里面找。
作用域小结
作用域是一套规则,用于确定在何处以及如何查找变量(标识符),如果查找的目标是对变量进行赋值,那么就会使用LHS查询,如果是获取变量的值,那么就会使用RHS查询。不成功的RHS查询抛出ReferenceError异常,不成功的LHS查询,(非严格模式下)会在全局自动创建一个同名变量并返回给引擎,严格模式下抛出ReferenceError异常。
通过引入LHS和RHS这两种向作用域询问的概念,应该就比较容易理解作用域是一套规则了。