作用域
一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第 3 天,点击查看活动详情。
《你不知道的javascript》 读书学习笔记打卡📒
1.什么是作用域?
书上描述作用域是一种规则。
几乎所有的编程语言都具备一种能力,也就是能够存储变量当中的值,并且能在之后对其进行访问和更改,但是这些变量是存储在哪里?并且之后程序又是如何找到它们? 这个如何存储决定了之后程序会以哪种规则去找到这个变量,而这种规则就是作用域
。
所以下面就来看下程序源代码在执行前,会对变量在哪里存储,以及如何进行存储(也就是如何制定某个变量得作用域)
1.1. 了解编译原理
传统编译语言,在程序中的一段源代码执行前会经历三个步骤,统称为“编译”
这里讨论javascript,javascript引擎进行编译的步骤和传统的编译语言是很相似的,但是在有些地方可能要复杂的多,比如词法分析和代码生成阶段由特定的步骤来对运行性能进行优化,包括对冗余元素进行优化等
接着详细介绍下上面的三个步骤
var a = 2;
-
分词/词法分析 在这个阶段会将字符组成的字符串分解成有意义的代码快,这些代码快会被成为词法单元(token)。像
var a = 2;
。这段程序会被分解成var、 a、=、2、;
(空格取决于在语言中是否起到作用) -
解析/语法分析 parsing 这个过程是将词法单元流(是个数组)转换成一个由元素逐级嵌套所组成的代表了程序语法结构的树。(抽象语法树 AST)
tokens[数组]
[
{
"type": "Keyword",
"value": "var"
},
{
"type": "Identifier",
"value": "a"
},
{
"type": "Punctuator",
"value": "="
},
{
"type": "Numeric",
"value": "2"
},
{
"type": "Punctuator",
"value": ";"
}
]
AST [转换成对象更清楚些]
{
"type": "Program",
"body": [
{
"type": "VariableDeclaration",
"declarations": [
{
"type": "VariableDeclarator",
"id": {
"type": "Identifier",
"name": "a"
},
"init": {
"type": "Literal",
"value": 2,
"raw": "2"
}
}
],
"kind": "var"
}
],
"sourceType": "script"
}
- 代码生成 将AST转换成可执行的代码得过程叫做代码生成
将var a = 2;
的AST转化成一组机器指令,用来创建一个a的变量,并为它分配内存,最后将一个值也就是2,存到a中。
1.2. 理解作用域
上面我们了解了编译语言在执行前会有的三个步骤,但是一直没说到作用域那块。最开始说的在哪里存储,如何存储,如何查找,现在让我们来了解一下作用域吧。
首先了解下几个重要的组成部分:
- 引擎 复杂javascript程序的编译和执行过程
- 编译器
负责编译前的准备工作,也就是前面说的
编译
流程 - 作用域 负责收集并维护由所以声明的标志符(变量)组成的一系列查找,并执行非常严重的规则---去确定当前执行的代码对这些标志符(变量)的访问权限。
再次拿下面这句代码作为例子
var a = 2;
当引擎看到这句代码时,它会认为这句代码是有两个完全不一样的声明
- 一个是由编译器在编译时处理
- 一个则是由引擎自己处理
来看下具体协同流程:
-
首先当然是编译器会将这段diam分解成词法单元,然后生成AST树结构。
-
在生成代码得过程中,首先会遇到
var a
,如果已经由一个该名称的变量存在于同一个作用域集合中,那么编译器会忽略该声明,继续编译。如果没有声明,那么它会要求在当前作用域的集合里面声明一个新的变量
,命名为a -
接着编译器将完成代码生成这一步骤,这些代码会被用来处理
a = 2
,也就是这个赋值的操作。 -
最后交给引擎来执行,引擎首先也会查找,在当前作用域集合内是否存在一个叫a的变量,如果有,就使用。
如果没有就继续查找
(这边是查找哦,那么如何查找呢?等会会说到),如果最终找到了a变量,就为它赋值,否则就会抛出一个错误异常。
总结下:变量得赋值操作会有两步操作
- 编译器会在当前作用域中声明一个变量(存在就不用)
- 运行时引擎再去作用域中找,如果找到就赋值,没找到报错
1.3 引擎查找变量过程
上面说了,引擎在执行代码得时候,会去作用域里面查找看下是否有这个变量,如果没有它会继续查找,那么它是如何查找的呢?
查询分成两种,一种是LHS,一种的RHS,是一个赋值的左侧或者右侧。
当变量出现在赋值操作的左侧进行LHS查询,当变量出现在赋值操作的右侧的时候,就采用RHS查询。
更准确的说法:RHS查询与简单的查找某个变量得值是相似的,但是LHS是找到变量本身,从而对其赋值
console.log(a) // 我们只是需要查找到a的值,而并非a的本身,因此用的LHS
a = 2; // 我们需要找到a的本身,并且为它赋值成2,因此用的就是LHS
1.4 作用域嵌套
刚才说到的,如果引擎没有在当前作用域下找到比那里,会继续往外面的作用域找,举个例子。
function foo(a) {
console.log(a + b);
}
var b = 3;
foo(2);
在函数foo的作用域中,对b进行RHS查询是找不到的,因此引擎会往外找,找到全局作用域var b = 3
,找到了,就会拿去使用。
作用域的最外层是全局作用域,如果还没找到,就会报异常。
简单了解了一下变量赋值的一个流程,从编译
阶段,到执行,到查找作用域中的变量,大致了解了一些。