简单了解JavaScript作用域是什么?

161 阅读5分钟

JavaScript作用域是什么?

作用域几乎是所有编辑语言的基本功能之一,就是能够储存变量当中的值,并且后续可以对变量的值进行修改或者访问。

1. 编译原理

尽管大家通常将JS归类为“解释执行”或者“动态”的语言,但是实际上它也可以被看作是一门编译语言,只不过他和传统的编译语言不同,他不像传统编译语言一样需要提前编译,编译结果也不可以在分布式系统中移植。但是JavaScript引擎进行编译的步骤和传统的编译语言很相似,在某些环节可能比预想中的还要更为复杂。

在传统编译语言流中,程序中一段代码在执行前经历的三个步骤,被统称为“编译”

分词/语法分析 -> 解析/语法解析 -> 代码生成

比起那些编译过程只有三步的语言编译器来说JavaScript引擎要复杂的多。例如在语法分析和代码生成阶段有特定的步骤对运行性能、冗余元素进行优化。因为JavaScript代码编译通常在代码运行前几微秒甚至在代码运行中所以不会像其他语言编译器拥有大量的实践进行优化。JavaScript引擎用尽了各种办法(例如JIT,可以延迟编译甚至实施重编译)来保证性能的最佳

2. 理解作用域

在这里我们学习作用域时将整个过程模拟成三位同学对话来进行理解,他们分别是

引擎

从头到尾负责整个JavaScript程序的编译及执行

编译器

负责语法分析以及代码生成等工作

作用域

收集维护所有声明的变量组成的一系列查询,它有一套严格的规则,确定当前执行的代码是否有权访问这些变量

当我们看到 var a = 1; 可能会理解为这是一个声明语句,由我们的编译器同学将程序分解为词法单元,然后将词法单元解析成一个树,但是编译器生成代码时可能与我们的预期不太一样。根据代码我们可以假设编译器同学首先为 一个变量分配内存并且命名为 a 然后将值2保存到当前变量中,但是这个假设并不是完全正确。

所以我们的引擎同学提出了不一样的见解,认为这是两个完全不同的声明,一个在编译器编译时处理,一个由引擎运行时处理。 接下来我们根据引擎同学的思想去解析var a = 1;,首先是 var a 这个部分编译器同学会询问作用域同学是否见到过这个名称的变量,如果存在编译器会忽略该声明,否则他会让作用域同学帮他创建一个新的变量命名a然后编译器同学为引擎同学生成运行所需代码,这些代码用来处理 a = 2这个赋值操作。代码生成完成后引擎同学会问作用域同学是否见到过a这个变量,如果当前作用域同学如果说没见到,引擎同学就会去问当前作用域同学的上一层作用域,直到找到a变量完成赋值操作,否则引擎同学就会说他遇到一个错误无法进行下一步。不愧是负责整个程序编译和执行的引擎同学提供了非常完美的答案。

但是为了为了刚好的理解引擎同学的说法咱们需要请编译器同学介绍一些编译器术语了。编译器在编译过程中第二步生成了代码,引擎执行时会告知作用域帮忙查找变量是否已经被声明,但是引擎执行怎样的查找,会影响最终的查找结果。就拿咱们刚才的例子,引擎会对变量a进行LHS查询,另外还有一个查找类型叫做RHS。LHS/RHS他们俩代表了赋值操作的左侧和右侧,用通俗的话来说就是当变量出现在赋值操作的左侧就是LHS,如果出现在右侧就是RHS。但是赋值操作符有很多类型,因此概念上最好理解的方式就是“操作目标是谁(LHS)”以及“赋值操作的源头(RHS)”

3. 作用域嵌套

刚刚上面我们了解到作用域根据名称查找变量的一套规则,但是实际我们需要同时考虑几个作用域。 当一个代码块或者函数嵌套在另一个代码块或者函数中就发生了作用域的嵌套,因此当前作用域无法找到该变量引擎就会去当前作用域上一级作用域进行查找,直到找到变量或者到达最外层作用域(全局作用域)为止 例如下面的代码

 function foo(a) {
     console.log(a + b);
 };
 var b = 2;
 foo(2) // 2

上面代码在foo函数中无法对b变量进行RHS查询,但是可以在foo作用域上一级作用域中完成