《你不知道的js》之作用域是什么

前沿

面试被问到闭包、作用域等一系列的问题,我下定决心我要把它拿下!

一、作用域是什么

定义:设计良好的规则来存储变量,并且之后可以方便地找到这些变量。

1.1 作用域的规则是什么

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

  • 分词/词法分析
    这个过程会将由字符组成的字符串分解成(对编程语言来说)有意义的代码块,这些代码块被称为词法单元(token)。例如:var a = 2;这段程序会被分解成为这些词法单元:var、a、=、2.空格是否会被当作词法单元,取决于空格在这门语言中是否有意义。
  • 解析/语法分析(Parsing)
    这个过程是将词法单元流(数组)转换成一个由元素逐级嵌套所组成的代表了程序语法结构的树。这个树被称为“抽象语法树”(Abstract Syntax Tree,AST)。
  • 代码生产
    将AST转换为可执行代码的过程被称为代码生成。这个过程与语言、目标平台等息息相关。抛开具体细节,简单来说就是有某种方法可以将var a = 2;的AST转化为一组机器指令,用来创建一个叫作a的变量(包括内存分配等),并将一个值储存在a中。

1.2 理解作用域

首先,我们要明白三个概念

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

下面我们将var a = 2;分解,看看引擎、编译器、作用域是如何协同工作的:
编译器首先会将这段程序分解成词法单元。然后将词法单元解析成一个树结构。但是当编译器开始进行代码生成时,它对这段程序的处理方式会和预期的有所不同。
可以合理的假设编译器所产生的代码能够用下面的伪代码进行概括:“为一个变量分配内存,将其命名为a,然后将值2保存进这个变量。”然而,这并不完全正确。

事实上编译器会进行如下处理:
1、遇到var a,编译器会询问作用域是否已经有一个该名称的变量存在于用一个作用域的集合中,如果是,编译器会忽略该声明,继续进行编译,否则它会要求作用域在当前作用域的集合中声明一个新的变量,并命名为a。(编译时声明)
2、接下来编译器会为引擎生成运行时所需的代码,这些代码被用来处理a = 2这个赋值操作。引擎运行时会首先询问作用域,在当前的作用域集合中是否存在一个叫作a的变量。如果是,引擎就会使用这个变量,如果否,引擎就会继续查找这个变量。(引擎运行时处理)

如果引擎找到了a变量,就会将2赋值给它。否则就会抛出异常。

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

1.3 引擎如何查找变量

引擎查找变量的两种方式:
LHS:赋值操作的左侧。
RHS:右侧,可以理解为“取到它的源值”或“得到某某值”。

1.4 作用域嵌套

当一个块或函数嵌套在另一个块或函数中时,就发生了作用域嵌套。因此,在当前作用域中无法找到某个变量时,引擎就会在外层嵌套的作用域中继续查找,直到找到该变量,或抵达最外层的作用域(也就是全局作用域)为止。

1.4 异常(为什么区分LHS和RHS是一件很重要的事情)?

如果RHS查询在所有嵌套的作用域中遍寻不到所需的变量,引擎就会抛出ReferebceError;当引擎执行LHS查询时。如果在顶层(全局作用域)中也无法找到目标变量,全局作用域中就会创建一个具有该名称的变量,并将其返还给引擎,前提是程序运行在非“严格模式”下(严格模式下查询失败会抛出异常)。
如果RHS找到了一个变量。但是你尝试对这个变量的值进行不合理的操作。比如试图对一个非函数类型的值进行函数调用,或者应用null或undefeated类型的值中的属性,那么引擎会抛出另一个类型的异常,叫作TypeError。

此处评论已关闭