作用域是什么?
# 作用域是什么?
# 编译原理
尽管通常将 JavaScript 归类为“动态”或“解释执行”语言,但事实上它是一门编译语言。 与传统的编译语言不同,它不是提前编译的,编译结果也不能在分布式系统中进行移植。 在传统编译语言的流程中,程序中的一段源代码在执行之前会经历三个步骤,统称为“编译”
- 分词/词法分析 (Tokenizing/Lexing)
这个过程会将“由字符组成的字符串分解成代码块”,例如 "var a = 10;" 这行代码通常会被分解成为"var、a、=、2、;" 分解为这种 操作符、 变量名、运算符、值、分号 或者 变量名、运算符、值、分号
- 解析/语法分析 (parsing)
这个过程是将词法单元流(数组)转换成一个程序语法树。这个树被称为”抽象语法树“。 再 "var a = 10;"的抽象语法树中可能会有一个叫做 VariableDeclaration 的顶级节点,然后是 Identifier(值是 a)的子节点,以及一个叫做 AssignmentExpression 的子节点。 AssignmentExpression 节点有一个叫做 NumericaLiteral(值为 10)的子节点。
- 代码生成
将 AST 转换为可执行代码的过程称为代码生成。简单来说就是有某种方法将 "var a = 10;"的 AST 转化为一组机器指令,用来创建一个叫做 a 的变量(包括分配内存等),并将一个值储存在 a 中。
但是 JavaScript 引擎要复杂更多。例如,在语法分析和代码生成阶段有特定的步骤来对运行性能进行优化,包括对冗余元素进行优化等。 JavaScript 引擎不会有大量的(像其他语言编译器那么多的)时间用来进行优化,因为与其他语言不同,JavaScript 的编译过程不是发生在构建之前的。 对于 JavaScript 来说,大部分情况下编译发生在代码执行前的几微秒(甚至更短!)的时间内。在我们所要讨论的作用域背后,JavaScript 引擎用尽了各种 办法(比如 JIT,可以延迟编译甚至实施重编译)来保证性能最佳。
简单地说,任何 JavaScript 代码片段在执行前都要进行编译(通常就在执行前)。因此, JavaScript 编译器首先会对 var a = 2; 这段程序进行编译,然后做好执行它的准备,并且 通常马上就会执行它。
# 要参与到对程序 var a = 10; 进行处理的过程中的成员。
- 引擎 从头到尾负责整个 JavaScript 程序的编译及执行过程。
- 编译器 引擎的好朋友之一,负责语法分析及代码生成等脏活累活。
- 作用域 引擎的另一位好朋友,负责收集并维护由所有声明的标识符(变量)组成的一系列查询,并实施一套非常严格的规则,确定当前执行的代码对这些标识符的访问权限。
下面我们将 var a = 10; 分解,看看引擎和它的朋友们是如何协同工作的。 编译器首先会将这段程序分解成词法单元,然后将词法单元解析成一个树结构。但是当编译器开始进行代码生成时,它对这段程序的处理方式会和预期的有所不同。 可以合理地假设编译器所产生的代码能够用下面的伪代码进行概括:“为一个变量分配内存,将其命名为 a ,然后将值 2 保存进这个变量。”然而,这并不完全正确。
事实上编译器会进行如下处理。
- 1、遇到 var a ,编译器会询问作用域是否已经有一个该名称的变量存在于同一个作用域的集合中。如果是,编译器会忽略该声明,继续进行编译;否则它会要求作用域在当前作 用域的集合中声明一个新的变量,并命名为 a 。 上面这句话简单来说就是,赋值和声明是两个过程,在声明前,如果作用域中存在该变量名称,则往下继续编译,如果不存在,那么进行变量声明,并将其命名为 a
- 2、接下来编译器会为引擎生成运行时所需的代码,这些代码被用来处理 a = 10 这个赋值操作。引擎运行时会首先询问作用域,在当前的作用域集合中是否存在一个叫作 a 的 变量。如果是,引擎就会使用这个变量;如果否,引擎会继续查找该变量。
总结:变量的赋值操作会执行两个动作,首先编译器会在当前作用域中声明一个变量(如果之前没有声明过),然后在运行时引擎会在作用域中查找该变量,如果能够找到就会对 它赋值。
引擎执行的查找方法
- LHS
- RHS 这个方法是查询的赋值操作 我的理解是,假设 我们的代码是 a = 10; 这行代码,这是赋值,需要用到变量名,而且 由于这是一个赋值操作,我根本不需要知道这个 a 在之前是多少,我只知道,在执行完我这行代码之后 a = 10; 只需要用到变量名,也就是赋值操作的左边 这就是 LHS。
那什么是 RHS 呢? 比方说 var a = 1;var b = 2; var s = a + b; 我们的 s 的值是要通过 a 和 b 决定的,那么 a 和 b 是多少呢? 是不是只需要知道 a、b 对应的值就好了 也就是 a = 1, b = 2 的右边 对吧?
数中给出的解释是: LHS 和 RHS 的含义是“赋值操作的左侧或右侧”并不一定意味着就是“ =赋值操作符的左侧或右侧”。赋值操作还有其他几种形式,因此在概念上最 好将其理解为“赋值操作的目标是谁(LHS)”以及“谁是赋值操作的源头(RHS)”。
考虑下面的程序,其中既有 LHS 也有 RHS 引用:
function foo(a) {
console.log( a ); // 2
}
foo( 2 );
首先看最后一行 foo(2); 一个函数的执行,我们是不是要先找到 foo ,通过 RHS 找到 foo 的值,且 foo 最好能是一个 function 类型,然后进入函数体内部, 打印了 a 的值,同样也要用 RHS 来 查找 a 对应的值。是正确的嘛? 肯定不是,由于函数接受参数 a 所以当 2 被当作参数传递给 foo 函数时,会被分配给参数 a,这个操作是隐式的,需要进行一次 LHS 查询。 然后才会用 RHS 来查找 a,然后 a 将值传递给 console.log。 console.log 本身也是一个引用,所以要对 console 进行一次 RHS 查找,而且要判断 找到的 console 中是否有 log 的方法。
# 作用域
假设我们有这样一个函数 foo
function foo(a){
console.log(a); // 2
}
foo(2);
引擎和作用域之间的对话;
引擎:我要为 foo 进行 RHS查找,作用域你见过 foo 嘛?
作用域:见过,编译器那小子刚刚声明了,它是一个函数,给你。
引擎:好的,我要执行一下 foo 函数。
引擎:hey bro,我要对 a 进行LHS查找,你见过 a 嘛?
作用域:见过,编译器把他声明成 foo 的形参,拿去吧。
引擎:好,接下来我要将 2 赋值给 a 。
引擎:哥们,我又来了,请问你见过 console 嘛,我要对他进行 RHS 查找。
作用域:见过,这是个内置对象,给你。
引擎:好,我看下有无 log 并且是不是一个函数。很好 是个函数,我要调用了。
引擎:bro ,帮我再找下 a 的RHS引用,虽然我见过他。
作用域:ok ,a的值还是2没有变化,拿去用把。
引擎:好,我要把 a 的值 2 传递给 log.
但是,实际上,作用域再查找名称时,通常要查找多个作用域。 当一个块或函数嵌套再另一个块或函数中时,就发生了作用域的嵌套。因此,再当前作用域中无法找到某个变量时,引擎就会再外层嵌套的作用域中继续查找,直到找到该变量, 或抵达最外层的作用域(即全局作用域)为止。
我们对 foo 函数进行一些修改
function foo(){
console.log(a)
}
var a = 2;
foo();
我们可以看到,在函数 foo 的作用域中,a 的 RHS 查找显然是不成功的,但是可以在上一级作用域,也就是全局作用域中完成。 也就是说,引擎和作用域之间的对话应该更丰富
引擎:foo 作用域,你见过 a 嘛,我要对他进行 RHS 查找。
foo 作用域:没见过。
引擎:foo 的上一级作用域,你见过 a 嘛,我需要对他进行 RHS 引用。
全局作用域:有的 ,给你。
遍历嵌套作用域链的规则很简单:引擎从当前的执行作用域开始查找变量,如果找不到, 就向上一级继续查找。当抵达最外层的全局作用域时,无论找到还是没找到,查找过程都 会停止。
# 异常
LHS 和 RHS 在进行未声明的变量的查找时,行为是不同的
function foo(a){
console.log(a+b);
b = a
}
foo(2);
RHS: 如代码所示,我们首次对 b 进行 RHS 查找肯定是找不到这个变量的,因为他并没有声明,所以在任何作用域中都无法找到他。 RHS查询在所有嵌套的作用域中都找不到所需要的变量时,引擎就会抛出一个 ** ReferenceError ** 异常。
LHS: 在进行LHS查询时,如果在所有嵌套的作用域中都无法找到的话,就会在全局作用域中创建一个,具有该名称的变量,并将其返还给引擎。 注意:这种情况只发生在非严格模式下。 在严格模式中,进制自动或隐式的创建全局变量。因此,并不会创建且返回一个全局变量给引擎,引擎会抛出同 RHS 查询失败时类似的 ReferenceError。
接下来,如果 RHS 查询找到了一个变量,但是你尝试对这个变量的值进行不合理的操作, 比如试图对一个非函数类型的值进行函数调用,或着引用 null 或 undefined 类型的值中的 属性,那么引擎会抛出另外一种类型的异常,叫作 TypeError 。
ReferenceError 同作用域判别失败相关,而 TypeError 则代表作用域判别成功了,但是对结果的操作是非法或不合理的。
作用域是一套规则,用于确定在何处以及如何查找变量。如果查找的目的是对变量赋值,则会使用LHS查找,如果目的是获取变量的值,则会使用RHS查询。
“=”操作符或调用函数传入参数的操作都会导致关联作用域的赋值操作。
JavaScript 引擎首先会在代码执行前对其进行编译,在这个过程中,像 var a = 2 这样的声明会被分解成两个独立的步骤:
- 首先,var a 在其作用域中声明新变量。这会在最开始的阶段,也就是代码执行前执行。
- 接下来,a = 2 会进行 LHS 查询, 查询变量 a 并对其进行赋值。