作用域是什么
编译原理
在传统编译语言的流程中,程序中的一段源代码在执行之前会经历三个步骤,统称为“编译”。
分词/词法分析
将字符组成的字符串分解成(对编程语言来说)有意义的代码块,这代码块被称为词法单元。
例如 var a = 2;
会被分解为 var、a、=、;
空格也可能被当作词法单元,这取决于空格在改语言中是否有意义。
解析/语法分析
将词法单元流转换为一个由 元素逐级嵌套所组成的 代表了程序语法结构的树——抽象语法树(Abstract Syntax Tree,AST)。
举例 var a = 2;
的抽象语法树可能由 VariableDeclaration
的顶级节点,接下来是一个叫做 Identifier
到子节点(它的值是a),以及一个叫做 AssginmentExpression
的子节点,它的节点有一个叫做 NumericLiteral
(它的值是2)的子节点。
代码生成
即将AST转换为可执行代码的过程。简单地来说,任何JavaScript代码片段在执行前都要进行编译(通常就在执行前)
理解作用域
学习作用域的方式其实可以理解为几个人物之间的对话。
演员表
引擎
从头到尾负责整个JavaScript程序的编译及执行过程。
编译器
负责语法分析及代码生成等脏活累活
作用域
负责收集并维护所有声明的标识符(变量)组成的一系列查询,并实施一套严格的规则,确定当前执行的代码对这些标识符的访问权限。
对话
比如var a = 2;
变量的赋值过程会执行两个动作,首先编译器会在当前作用域中声明一个变量(如果之前没有声明过),然后在运行时引擎会在作用域中查找该变量,如果找到就会对它赋值。
编译器
编译器在编译过程的第二步中生成了代码,引擎执行它时,会通过查找变量a判断它是否已声明过,查找的过程由作用域进行协助,这时引擎会进行LHS查询和RHS查询。
LHS和RHS的含义是“赋值操作的左侧或右侧”并不一定意味着就是“=赋值操作的左侧或右侧”。可以理解为""以及""
例子如下所示:console.log(a);
其实a的引用是一个RHS引用,没有对a进行赋值而是查找并取得a值。相对比:a = 2;
则是对a的LHS引用,只是想要为2这个赋值找到一个目标为a。
引擎和作用域的对话
function foo(a) {
console.log(a); // 2
}
上述代码所述,引擎和作用域之间的操作如下:
引擎在进行对foo进行RHS时候会在作用域中查找(由于编译器声明了是个函数),然后在执行foo,执行的时候需要对a进行RHS引用,再次在作用域中查找(由于编译器声明对时候给foo了一个形式参数),引擎在执行console的RHS引用时也要在作用域中查找(console是作用域内的一个内置对象),然后引擎发现console有log方法,在执行a的RHS引用时还需在作用域中查找确认后在传递进log方法。
作用域嵌套
当时,就会发生作用域的嵌套。若是当前作用域内无法找到某个变量,引擎就会在外层嵌套的作用域中继续查找,直到找到变量,或抵达最外层的作用域(即全局作用域)为止。
异常
在变量还没声明(即在任何作用域中都无法找到该变量)的情况下,LHS和RHS是有区别的,如下代码所示。
function foo(a) {
console.log( a + b );
b = a;
}
foo(2)
第一次对b进行RHS查询的时候是找不到该变量的,引擎会抛出ReferenceError
异常。而引擎执行LHS查询,若找不到会在全局作用域中创建该变量,并返还(前提是在非严格模式下),严格模式下则不会创建。
小结
作用域是,用于(标识符),如果查找的目的是对,则进行,若是,则进行。
更简洁的话来说就是, 作用域定义一套规则,这套规则用来 管理引擎如何在当前作用域以及嵌套的子作用域中根据标识符名词进行变量查找。