《你不知道的JavaScript-上》作用域是什么

编译原理

作用域:是一套设计良好的规则,用来存储变量,并且之后可以方便地找到这些变量。

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

  1. 分词/词法分析(Tokenizing/Lexing):将程序中字符串分解成词法单元
  2. 解析/语法分析(Parsing):将词法单元流(数组)转换成一个元素逐级嵌套所组成的代表了程序语法结构的树,这个树成为抽象语法树(AST)
  3. 代码生成:将AST转换成可执行代码的过程称为代码生成

JavaScript的编译过程不是发生在构建之前,不会有大量时间来优化,任何JavaScript代码片段在执行前(几微秒)都要编译。

理解作用域

本书学习作用域的方式是将这个过程模拟成几个人物之间的对话。

演员表

首先介绍将参与到对程序var a=2;进行处理的过程中的演员们:

  • 引擎
    从头到尾负责整个JavaScript程序的编译及执行过程
  • 编译器
    负责语法分析及代码生成
  • 作用域
    负责收集并维护所有声明的标识符(变量)组成的一系列查询,并实施一套非常严格的规则,确定当前执行的代码对这些标识符的访问权限。

对话

面对var a=2;时,引擎会认为这里有两个完全不同的声明:
一是由编译器在编译时处理;
二是则由引擎在运行时处理。

编译器、引擎的工作过程:

  1. 遇到var a,编译器首先查询作用域是否存在该名称变量,如果有,编译器则忽略该声明;否则声明一个新变量a;
  2. 接下来编译器会对a=2这个赋值操作进行代码生成工作。引擎运行时会首先询问作用域是否存在该变量,如果有,引擎会使用这个变量;如果没有,引擎继续查找,查不到就抛出一个异常。

总结:变量的赋值操作会执行两个动作,首先编译器会在当前作用域中声明一个变量,然后在运行时引擎会在作用域中查找该变量,如果能够找到就对它赋值。

编译器有话说

编译器在编译过程中代码生成后,引擎执行时,会通过查找变量a来判断它是否已经声明。查找的过程由作用域进行协助,但有两种查找类型:LHSRHS:
LHS查询是试图找到变量的容器本身,RHS查询就是简单的查找某个变量。

1
2
3
4
5
6
7
8
9
function foo(a){
var b=a;
return a+b;
}

var c=foo(2);

4个RHS:foo(..、=a、a..、..b
3个LHS:c=、a=2、b=..

作用域的嵌套

作用域是根据名称查找变量的一套规则。实际情况中,通常需要同时顾及几个作用域。
遍历嵌套作用域链的规则:引擎从当前执行的作用域开始查找变量,如果找不到就向上一级继续查找,当找到最外层的全局作用域时,无论找没找到,都会停止。

异常

为什么区分LHS和RHS是一件重要的事?
在变量还没有声明(任何作用域中都无法找到该变量)情况下,RHS是会抛出ReferenceError异常;LHS则是在全局作用域中创建一个具有该名称的变量,并返给引擎(这就是隐式的全局变量的由来)。

严格模式下,会禁止自动或隐式的创建全局变量,所有LHS查询失败时也会抛出ReferenceError异常。

当LHS和RHS查询成功,但对变量的值进行不合理的操作,会抛出TypeError。

小结

作用域是一套规则,用于确定在何处以及如何查找变量(标识符)。
JavaScript引擎首先会在代码执行前对其进行编译,在这个过程中,像var a=2这样的声明会被分解成两个独立的步骤:

  1. 首先, var a 在其作用域中声明新变量。这会在最开始的阶段,也就是代码执行前进行。(变量声明提升的由来)。
  2. 接下来, a = 2 会查询(LHS查询)变量a 并对其进行赋值。

不成功的RHS引用会导致抛出ReferenceError异常。不成功的LHS引用会导致自动隐式地创建一个全局变量(非严格模式下) , 该变量使用LHS引用的目标作为标识符, 或者抛出ReferenceError 异常(严格模式下) 。

liborn wechat
欢迎您扫一扫上面的微信二维码,订阅我的公众号!