本文为You don’t know JavaScript学习笔记
什么是作用域
1. 编译原理(compiler theory)
传统编译语言(compiled-language)流程中,源代码在执行前会经历如下三个步骤:
- 分词/词法分析(Tokenizing/Lexing)
这个过程会将由字符组成的字符串分解为有意义的代码块,这些代码块被称为词法单元(token)。
如代码:
var a = 2;
会被分解为如下词法单元: var, a, =, 2,;。
其中空格取决于在该语言中是否有意义。
- 解析/语法分析(Parsing)
该过程会将词法单元流(数组)转换成一个由元素逐级嵌套组成的结构树。这个树被称为“抽象语法树”(Abstract Syntax Tree,AST)
- 代码生成(Code-Generation)
该过程将AST转换为可执行的代码,这个过程与语言,目标平台等有关。
具体为用某种方法讲
var a = 2;
的AST转化为一组机器指令,来创建一个叫做a的变量,并将一个值存储在a中。
JavaScript引擎的编译相对如上几个步骤来说要复杂很多,如在语法分析和生成代码的阶段会对运行性能进行优化,对冗余元素进行优化等。
由于JavaScript的编译过程不是发生在构建之前的,所以js引擎不会有大量的时间用来进行优化。大部分的情况下代码的编译发生在代码执行前 的几微秒。
2. 理解作用域
以对程序var a = 2;
的处理过程来了解,作用域。
2.1 首先需要理解如下组件:
- 引擎(Engine)
负责整个程序的编译和执行。
- 编译器(Compiler)
负责词法分析和代码生成。
- 作用域(Scope)
负责收集并维护所有的声明的标识符(变量)组成的一系列查询,并实施一套严格的规则, 确定当前执行的代码对这些标识符是否有访问权限。
2.2 组件间的对话
下面我们将var a = 2;
分解,观察引擎,编译器,作用域是如何交流的。
- 遇到
var a
时- step1: Compiler ->询问Scope是否有叫做a的变量在当前作用域下,如果Scope回答是,Compiler就会忽略该声明,继续编译; 否则Compiler会要求Scope在当前作用域下声明一个新的变量a。
- step2: Compiler ->为Engine生成运行是所需要的代码,处理
a = 2
的操作。- step3: Engine ->询问Scope当前作用域是否存在a这个变量,如果存在Engine会使用这个变量;如果不存在,Engine会继续查找 名叫a的变量。
- step4: Engine ->最终如果找到了a,就会将2赋值给他。否则Engine会抛出一个异常。
总结: 可以将上述4个步骤归结为两个动作,编译器在当前作用域下声明(如果当前作用域下没有该变量)一个变量, 然后引擎会在该作用域中查找该变量,如果存在那么进行赋值操作,如果不存在则抛出异常。
2.3 LHS&RHS
在上述例子中,Compiler生成代码后,Engine执行代码时,会通过查找当前Scope下的变量a判断他是否声明过。
例子中,Engine会对变量a进行LHS查询,另外一个查找类型叫做RHS。
可以简单理解为,当变量出现在一个赋值操作左侧的时候进行LHS查找,出现右侧时进行RHS查找。
准确点,RHS查询简单的理解为查询某个变量的值,而LHS查询则是试图找到变量的容器本身,从而可以对其赋值。
可以将RHS理解为retrieve his source value(获取他的源值)。
分析如下代码
console.log(a);
对
a
的引用是一个RHS引用。需要获取a的源值才能将其传递给console.log(...)
对比 a = 2;
对a引用则为LHS引用。此时我们不关心a当前源值为多少,我们只想为
= 2
这个赋值操作找到赋值对象即可。
因此在概念上最好理解为:
- LHS: 赋值操作的目标是谁? 即查询赋值操作的目标。
- RHS: 谁是赋值操作的源头? 即查询赋值变量的源值。
分析如下代码:
function foo(a){
console.log(a);
}
foo(2);
我们从函数的调用开始:
foo(...);
Engine首先会在当前Scope中对foo进行RHS引用。foo(2)
Engine会在当前Scope中对a进行LHS引用,给形式参数赋值,将2赋值给a。- 进入foo函数内部,Engine在当前Scope下对console进行RHS引用。
- 查找console是否有log函数,此时对a进行RHS引用,并将a的源值传递进log(…)
- …
3. 作用域嵌套(Nested Scope)
作用域是根据名称查找变量的一套规则。
当一个块或者一个函数嵌套在另一个块或者函数中时,就发生了作用域的嵌套。
因此在当前作用域中无法找到某个变量的时候,Engine就会在外层嵌套的作用域中进行查找,直到找到该变量 或者一直找到最外层作用域(全局作用域)为止。
分析如下代码:
function foo(a){
console.log(a + b);
}
var b = 2;
foo(2); // 4
在执行console.lgo(a + b)
时对a进行RHS引用找到a的值为2,对b进行RHS引用时在当前作用域中无法完成
但是在上一级作用域中可以完成引用。
遍历作用域的规则: Engine从当前执行的作用域下开始查找,如果没有找到,就会进入上一级作用域继续查找。当遍历到最外层 作用域的时候,无论找到与否都会停止查找。
4. 异常(Exception)
分析如下代码:
function foo(a){
console.log(a + b);
b = a;
}
foo(2);
当第一次对b进行RHS引用时是无法找到该变量的,因此可以说这是一个未声明的变量。
如果RHS查询,遍历到了最外层作用域即全局作用域也没有找到目标变量时,Engine会抛出ReferenceError 异常。
相较之下,LHS查询,如果在全局作用域中没有找到目标变量,全局作用域下创建一个具有该名称的变量,并 返还给Engine(只有在非严格模式下才会这样,严格模式下会抛出异常)。
小结
JavaScript编译流程,大致分为几个步骤?
大致分为三个步骤,但是实际的编译过程比较复杂。
了解引擎,编译器,作用域如何工作,以及他们各自的作用。
了解作用域中的查找规则(LHS&RHS)以及在嵌套作用域中的查找规则。