第一部分 作用域和闭包
第一章 作用域是什么
1.1 编译原理
传统编译的三个步骤
- 1,分词/词法分析(Tokenizing/Lexing) : 这个过程会将由字符组成的字符串分解成有意义的代码块,这些代码块被称为词法单元(token)。例如,var a = 2;,通常会被分解成这些词法单元:var、a、=、2、;。
- 2,解析/语法分析(Parsing): 这个过程是将词法单元流(数组)转换成一个由元素逐级嵌套所组成的代表了程序语法结构的树。这个树被称为“抽象语法树”(Abstract Syntax Tree,AST)。例如var a = 2 的抽象语法树:
- 3,代码生成:将
AST转换为可执行的代码, 简单的说就是用某种方法可以将var a = 2的AST转换为一组机器指令, 用来创建一个叫做a的变量(包括分配内存), 并将一个值储存在a中.
1.2 理解作用域
1.2.1 代码编译到执行的参与者
对程序 var a = 2; 进行处理的过程,需要以下参与
- 引擎
负责整个 JavaScript 程序的编译及执行过程(核心)
- 编译器
负责语法分析及代码生成等(编译三步)
- 作用域
负责收集并维护由所有变量组成的查询,并确定访问权限
1.2.2 对话(代码编译执行过程)
引擎 有一天看见了一段代码 var a = 2;,这段代码在引擎看来是两段完全不同的内容,所以引擎把这段代码拆成了两部分: 1:var a: ``2:a = 2,然后把第一段代码交给了编译器,编译器就拿着这段代码问作用域,你那有 a 这个变量吗?作用域如果说有,那么编译器就会忽略掉这段声明。否则进行 a 变量声明。接下来,编译器会为引擎生成运行时所需的代码,这些代码被用来处理 a = 2 这个赋值操作。引擎会首先询问作用域:在当前的作用域集合中是否存在一个叫作 a 的 变量。如果是,引擎就会使用这个变量。否则,引擎会继续查找该变量。
1.2.3 作用域的LHS查询和RHS查询
例如var a = b,引擎会对变量a和变量b都向作用域进行查询,查询分为两种:LHS和RHS,即对变量a进行LHS查询,对变量b进行RHS查询
- LHS是要查询到变量的声明或容器(而不是变量的值),从而后面可以为其赋值
- RHS是要查询到变量最终的值
- LHS 和 RHS 的含义是“赋值操作的左侧或右侧”并不一定意味着就是“= 赋值操作符的左侧或右侧”
1.2.4 引擎和作用域的对话
引擎进行LHS和RHS查询时都会找作用域要.
function foo(a) {
console.log( a ); // 2
}
foo( 2 );
让我们把上面这段代码的处理过程想象成一段对话,这段对话可能是下面这样的。
引擎:我说作用域,我需要为 foo 进行 RHS 引用。你见过它吗?
作用域:别说,我还真见过,编译器那小子刚刚声明了它。它是一个函数,给你。
引擎:哥们太够意思了!好吧,我来执行一下 foo。
引擎:作用域,还有个事儿。我需要为 a 进行 LHS 引用,这个你见过吗?
作用域:这个也见过,编译器最近把它声名为 foo 的一个形式参数了,拿去吧。
引擎:大恩不言谢,你总是这么棒。现在我要把 2 赋值给 a。
引擎:哥们,不好意思又来打扰你。我要为 console 进行 RHS 引用,你见过它吗?
作用域:咱俩谁跟谁啊,再说我就是干这个。这个我也有,console 是个内置对象。 给你。
引擎:么么哒。我得看看这里面是不是有 log(..)。太好了,找到了,是一个函数。
引擎:哥们,能帮我再找一下对 a 的 RHS 引用吗?虽然我记得它,但想再确认一次。
作用域:放心吧,这个变量没有变动过,拿走,不谢。
引擎:真棒。我来把 a 的值,也就是 2,传递进 log(..)。
上述的查询步骤为:
- RHS:foo(2)
- LHS:a = 2
- RHS:console
- RHS:xxx.log(a)
1.3作用域嵌套
当一个块或函数嵌套在另一个块或函数中时,就发生了作用域的嵌套,进而形成了一条作用域链。因此,在当前作用域中无法找到某个变量时,引擎就会在外层嵌套的作用域中继续查找,直到找到该变量 ,或抵达最外层的作用域(也就是全局作用域)为止。
当引擎需要对作用域进行查询时.引擎会从当前的执行作用域开始查找变量,如果找不到, 就向上一级继续查找。当抵达最外层的全局作用域时,无论找到还是没找到,查找过程都会停止。
例如下面的代码中,我们在 foo 函数中写入了一个 bar 函数。那么此时在这段代码中就存在 3 个作用域:
- 全局作用域
foo函数作用域bar函数作用域
这三个作用域组合在一起,就叫做 作用域嵌套。
当我们在 bar 函数中访问变量 a 的时候,bar 函数作用域中没有 a 变量,所以会向上层进行查找,直到在 foo 作用域中找到 a 变量。 那么这种逐层向上层作用域查找的机制,就是作用域嵌套时变量查找机制。
1.4 异常
在 逐层向上 的查找过程中,引擎会从变量当前作用域开始,一直查找到全局作用域,如果 RHS 查询在所有嵌套的作用域中遍寻不到所需的变量或函数,引擎就会抛出 ReferenceError 异常。例如下面例子中console.log(a+b)由于RHS此时是找不到b的值,故会抛出ReferenceError
function foo(a) {
console.log( a + b );
b = a;
}
foo( 2 );// Uncaught ReferenceError: b is not defined
如果 RHS查询找到了一个变量,但是你尝试对这个变量的值进行不合理的操作, 比如试图对一个非函数类型的值进行函数调用,或着引用null 或 undefined类型的值中的 属性,那么引擎会抛出TypeError异常 。
- 程序运行在非 “严格模式”下,当引擎执行 LHS 查询时,如果在顶层(全局作用域)中也无法找到目标变量,全局作用域中就会创建一个具有该名称的变量,并将其返还给引擎
- 在严格模式中 LHS 查询失败时,并不会创建并返回一个全局变量,引擎会抛出同RHS查询失败时类似的 ReferenceError 异常。
1.5 LHS与RHS小结
- LHS和RHS查询都是引擎对作用域的查询
- LHS和RHS都会沿着作用域链进行查询,直到最上层的全局作用域.如果没找到的话,在非严格模式下,LHS则会在全局创建一个相同名称的隐式全局变量,RHS则会抛出ReferenceError的异常
- 如果查找的目的是对变量进行赋值,那么就会使用 LHS 查询;如果目的是获取变量或函数的值,就会使用 RHS 查询。LHS只是找到变量的容器而已,方便进行赋值,RHS查询则需要找到变量的值
=操作符或调用函数时传入参数的操作都会导致关联作用域的赋值操作,此时都会进行LHS查询