第一章 作用域是什么
前言
最近在看一本不错的书《你不知道的JavaScript》,曾经作为前端程序员虽然每天都在使用JavaScript, 然而因为任务以结果导向,并没有花时间去理解,只是了解一些使用技巧,经常遇到棘手的问题时束手无策。这种囫囵吞枣的方式,让我一直如鲠在喉,我认为想要真正的熟悉JavaScript选择一本好书是非常重要的。而这本书的好处在于它不同于一般的工具书,用大段的篇幅去学习基本语法,而是旨在升华提高你对这门语言的理解,以及一些所谓JavaSript的隐秘角落。请不要小看这些所谓的隐秘角落,这些才是这门语言的精髓所在。如果不理解这些而只求用代码完成任务,那么你将永远停留在初学者的阶段。从今天开始,我将系统地阅读这门书,在这里做好笔记,既方便自己查看也方便小伙伴们一起交流。话不多说,我们一起学习吧。
作用域是什么
简单的说作用域为程序提供了状态,它能够存储变量中的值,并且能再之后对这个值进行访问和修改。那么什么是作用域? 作用域是一套良好的设计规则用来存储变量,并且之后能方便的找到这些变量。
编译原理
JavaScript通常归类为动态或解释执行语言,但其实它是一门编译语言。这部分依然存在争议,那么什么是解释执行语言,什么是编译语言呢?
- 编译型语言:编译型语言的首先将源代码编译生成机器码,再由机器运行机器码(二进制)。例如C/C++等等。程序在执行之前需要一个专门的编译过程,把程序编译成为机器语言的文件,运行时不需要重新翻译,直接使用编译的结果就行了。程序执行效率高,依赖编译器,跨平台性差些。
- 解释执行语言:执行源代码直接由解释器对其进行解释运行。例如python,shell等等。程序不需要编译,程序在运行时才翻译成机器语言,每执行一次都要翻译一次。因此效率比较低。
JavaScript与传统的编译语言不通,它不是提前编译的,编译结果也不能在分布式系统中进行移植。
传统编译语言中,源代码执行前要经历三个步骤。
- 分词/词法分析(Tokenizing/Lexing): 这个过程会将由字符组成的字符串分解成有意义的代码块,这些代码被称为词法单元(token)
- 解析/语法分析(parsing): 这个过程是将词法单元流(数组)转换成一个由元素主机嵌套所组成的程序语法结构树。这个树被称为抽象语法树(Abstract Syntax Tree, AST)
- 代码生成:将AST转换成为可执行代码的过程。这个过程与语言,目标平台相关。抛开具体细节,简单来说,就是有某种方法可以讲var a = 2;的AST转化成一组机器指令,用来创建一个叫做a的变量,并将一个只存储在a中。
JavaScript引擎要复杂得多,他不会像其他编译语言一样有充足的时间来优化,他的编译过程发生在代码执行前的几微秒。在我们讨论的作用域背后,JavaScript引擎用尽各种方法(JIT)来保证性能。简单说JavaScript代码片段在执行前都要进行编译。
理解作用域
程序的处理需要三方合作,分别是引擎,编译器和作用域。
- 引擎:负责整个JavaScript的编译以及执行过程
- 编译器: 负责语法分析以及代码生成
- 作用域: 负责收集并维护所有声明的标识符(变量)组成的一系列查询,并实施一套严格的规则,确定当前执行的代码对这些标识符的访问权限。
当需要处理一段程序的时候,流程是这样的。
- 编译器首先会将这段程序分解成词法单元,然后将词法单元解析成一个树结构,紧接着开始生成代码。
- 编译器生成代码时与一般的预期有所不同,这里编译器会先询问作用域是否已经有相同名称的变量存在于同一个作用域的集合中。如果是,编译器会忽略该声明,继续进行编译;否则他会要求作用域在当前作用域的集合中声明一个新的变量,并以该名称命名。
- 接下来编译器会为引擎生成运行时所需的代码,引擎运行时会询问作用域,在当前作用域集合是否存在相同名称的变量,如果是,引擎会使用这个变量;否则,引擎会继续查找,如果最终没有找到,则会抛出异常。
拓展一下
引擎在执行编译器生成的代码时,会执行查询变量操作来判断该变量是否已经被声明。该查询过程是由作用域协作完成的。这里有两种查询类型,即LHS和RHS查询。这个很好理解,一般来说LHS和RHS以赋值符号为界限。
- LHS: 出现在赋值符号左侧,为赋值符号右边的值赋值到一个容器里面去,可以理解为存储操作。
- RHS: 笼统地说出现在赋值符号的右侧,但实际上很多情况下是隐式的。可以理解为找到某个值的源头,所以是取值操作。
LHS和RHS的含义是“赋值操作的左侧或右侧”并不一定意味着就是“=赋值操作符的左侧或右侧”。赋值操作还有其他几种形式,因此在概念上最好将其理解为“赋值操作的目标是谁(LHS)”以及“谁是赋值操作的源头(RHS)”。
作用域嵌套
作用域嵌套的概念不难理解,一个作用域里面有可能会嵌套另一个作用域。因此在当前作用域里面无法找到某个变量时,引擎就会在外层嵌套的作用域中继续查找,直到找到该变量,或者抵达最外层的作用域(全剧作用域)为止。
异常
这个小节简单的讨论一下异常。在RHS(取值)查询操作中,如果无法找到该变量,那么将会返回ReferenceError,因为其是未声明变量。而在LHS(存储)查询操作中,如果无法找到该目标变量,则有两种可能:非严格模式下,全剧作用域会创建一个具有该名称的变量,并将其返回给引擎;而严格模式下,将不会创建全局变量,而是抛出ReferenceError查询失败的异常。
如果RHS查询找到了一个变量,但是你对这个变量的值进行不合理操作,例如试图对一个非汉属类型的值进行函数调用,或者引用null, undefin类型中的属性,那么引擎会抛出另外一种类型的异常, 叫做TypeError。
ReferenceError同作用域判别失败相关,而TypeError则代表作用域判别成功,但是对结果操作是非法的。