作用域是什么
编译原理
在传统的编译语言中,程序中的一段源代码在执行之前会经历三个步骤。成为编译
- 分词/词法分析
这个过程由字符组成的字符串分解成有意义的代码块,这些代码块成为词法单元。
分词和词法分析之间的主要差异在于词法单元的识别是有状态还是无状态的方式进行的,如果词法单元生成器在判断a是一个独立的词法单元还是被其他词法单元的一部分时,成为这个过程为词法分析
- 解析/语法分析
这个过程是将词法单元流转换为一个由元素逐级嵌套所组成的程序语法结构的树,这个树被称为抽象词法树 - 代码生成
将ast转换为可以执行的代码的过程叫做代码生成。
理解作用域
JavaScript引擎不会有大量的时间用来优化,JavaScript的编译过程不是发生在构建之前的。
名词
- 引擎: 从头到尾负责整个JavaScript程序的编译和执行过程
- 编译器: 词法分析 代码生成
- 作用域: 负责收集并维护所有声明的标识符组成的一系列的查询。
遇到var a,编译器会询问作用域是否已经有一个该名称的变量存在于同一个作用域的集合中,如果是,编译器会忽略该声明,继续进行编译,否则会要求作用域在当前作用域集合中声明一个新的变量,命名为a
接下来编译器会为引擎生成运行时所需的代码,这些代码会被用来处理a=2这个赋值的操作,引擎运行时会首先询问这个作用域,在当前作用域集合中是否存在一个叫做a的变量,如果是,引擎就会使用这个变量。如果否,引擎就会继续查找该变量。
引擎会为变量a进行LHS查询,另一个查找的类型叫做RHS,当变量出现在赋值操作的左侧进行LHS查询,出现在右侧的时候进行RHS查询。
RHS查询类似简单的查找某个变量的值,LHS则是试图找到变量的容器本身。
console.log(a)
其中对于a的引用是一个RHS引用,这里a并没有赋予任何值,需要查找a的值,才能够进行赋值。这里对a的引用是一个RHS引用。
a = 2 // 这里是一个LHS引用,实际上并不关心当前的值是什么,指示想要为这两个赋值找到一个目标。
function foo(a) {
console.log(a)
}
foo(2)
最后一行foo函数的调用需要对foo进行rhs引用。代码中的隐式的a=2操作可能容易被忽略掉,这个操作发生在2被当作参数传递给foo函数,2会被分配给参数a,为了给参数a分配值,需要进行一次LHS引用查询。
编译器还可以在代码生成的同时处理声明和值的定义。
作用域
当一个块或者函数嵌套在另一个块或者函数中,就发生了作用域的嵌套,在当前作用域中无法找到某个变量的时候,引擎就会在外层嵌套的作用域中继续查找,直到找到该变量,或者抵达最外层的作用域也就是全局作用域为止。
function foo(a) {
console.log( a + b )
}
var b = 2
foo( 2 )
对于b的rhs无法在函数foo内部完成,在上一级的作用域中去查找。
遍历嵌套作用域链的规则: 引擎从当前的执行作用域开始查找变量,如果找不到,就继续向上一级继续查找,当抵达最外层的全局作用域的时候,无论是找到还是没有找到,查找过程都会停止。
异常
在变量还没有声明的情况下, 这两种查询的行为是不一样的。
function foo(a) {
console.log(a + b)
b = a
}
foo(2)
第一次b进行RHS查询的时候都是无法找到该变量的,如果RHS在所有的嵌套的作用域中都找不到所需的变量,引擎就会抛出ReferenceError异常。
当进行LHS查询的时候,如果在顶层全局作用域中无法找到目标变量,全局作用域中就会创建一个具有该名称的变量,并将器返回给引擎,前提是程序运行在非严格模式下。
如果查找的目的是对变量进行赋值,那么就会使用LHS查询;如果目的是获取变量的值,就会使用RHS查询。
不成功的RHS会导致抛出ReferenceError异常,不成功的LHS会导致自动隐式的创建一个全局变量(非严格模式下)。使用LHS引用的目标作为标识符,或者抛出ReferenceError异常(严格模式下)