小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。
本文已参与「掘力星计划」,赢取创作大礼包,挑战创作激励金。
在js 中,每个变量是有状态的,比如分为全局变量或局部变量,变量声明或变量未声明。
如果变量没有状态,或许可以执行简单的操作,但是复杂操作会受到限制。
变量存储在哪里?程序需要时,如何找到它?
于是,就需要设计一套良好的规则来存储变量,方便以后更容易找到变量。
这套规则就是 “ 作用域 ” 。
编译原理
通常将 JavaScript 归类为“动态”或“解释执行”语言,但事实上它 是一门编译语言。
与传统的编译语言不同,JavaScript 不是提前编译的。大部分情况下编译发生在代码执行前的几微秒(甚至更短)的时间内。
传统编译语言的流程
在传统编译语言的流程中,程序中的一段源代码在执行之前会经历三个步骤,统称为“编译”
- 分词/词法分析(Tokenizing/Lexing)
- 这个过程会将由字符组成的字符串分解成(对编程语言来说)有意义的代码块,这些代码块被称为词法单元(token)。
- 解析/语法分析(Parsing)
- 这个过程是将词法单元流(数组)转换成一个由元素逐级嵌套所组成的代表了程序语法结构的树。这个树被称为“抽象语法树”(Abstract SyntaxTree, AST)。
- 代码生成
- 将AST转换为可执行代码的过程被称为代码生成。这个过程与语言、目标平台等息息相关。
JS中编译原理
比起那些编译过程只有三个步骤的语言的编译器,JavaScript引擎要复杂得多。例如,在语法分析和代码生成阶段有特定的步骤来对运行性能进行优化,包括对冗余元素进行优化等。
🎈举个例子:
var name = 'wuyanfeiying';
- 分词/词法分析
将整个代码字符串分割成最小语法单元数组, 每一个元素都是一个最小的语法单元。
Tokens
[ { "type": "Keyword", "value": "var" }, { "type": "Identifier", "value": "name" }, { "type": "Punctuator", "value": "=" }, { "type": "String", "value": "'wuyanfeiying'" }]
- 解析/语法分析
在分词基础上建立分析语法单元之间的关系, 把分词的结果按照相互的关系组成一个树形结构
SyntaxTree
{
"type": "Program",
"body": [
{
"type": "VariableDeclaration",
"declarations": [
{
"type": "VariableDeclarator",
"id": {
"type": "Identifier",
"name": "name"
},
"init": {
"type": "Literal",
"value": "wuyanfeiying",
"raw": "'wuyanfeiying'"
}
}
],
"kind": "var"
}
],
"sourceType": "script"
}
- 代码生成 将AST转换为可执行代码的过程被称为代码生成。
将var name = 'wuyanfeiying';的AST转化为一组机器指令,用来创建一个叫作 name 的变量(包括分配内存等),并将一个值储存在 name 中。
理解作用域
先来理解几个概念:
引擎
负责整个JavaScript程序的编译及执行过程。
编译器
负责语法分析及代码生成等工作。
作用域
负责收集并维护由所有声明的标识符(变量)组成的一系列查询,并实施一套非常严格的规则,确定当前执行的代码对这些标识符的访问权限。
🎈当程序中有这么一段代码的时候:
var name = 'wuyanfeiying';,会进入以下两个阶段:
- 阶段1,主角
编译器:- 在 编译时 ,会在同一个作用域的集合中,寻找是否有该名称的变量。
- 有,编译器会忽略这个声明,继续编译。
- 没有,当前作用域会声明一个新的名为
name的变量。 紧接着,编译器会为引擎生成运行时所需的代码, 这些代码被用来处理var name = 'wuyanfeiying';这个赋值操作。
- 在 编译时 ,会在同一个作用域的集合中,寻找是否有该名称的变量。
- 阶段2,主角
引擎:- 引擎在运行时,首先会在当前的作用域集合中寻找是否存在一个叫作
name的变量。- 如果是,引擎就会使用这个变量;
- 如果否,引擎会继续查找该变量。
- 引擎在运行时,首先会在当前的作用域集合中寻找是否存在一个叫作
如果引擎最终找到了name变量,就会将'wuyanfeiying'赋值给它。
否则引擎就会抛出一个异常!
LHS 和 RHS
在执行一段代码 var name = 'wuyanfeiying'; 的时候,编译器会为引擎生成运行时所需的代码,
引擎执行它时,会通过查找变量 name 来判断它是否已声明过。
查找的过程由作用域进行协助,但是 引擎执行怎样的查找,会影响最终的查找结果 。
LHS,RHS 这两个术语就是出现在引擎对变量进行查询的时候。
有两种查询方式:
- LHS(Left-hand Side): 赋值操作的目标是谁
- RHS(Right-hand Side): 谁是赋值操作的源头
《你不知道的JavaScript(上卷)》中的🌰 寻找其中所有的 LHS 和 RHS:
function foo(a) {
var b = a;
return a + b;
}
var c = foo(2);
🎈自己的理解:
如果查找的目的是对变量进行赋值,那么就会使用LHS查询;如果目的是获取变量的值,就会使用RHS查询。
作用域链
前面已经讲了,作用域是根据名称查找变量的一套规则。
如果引擎在当前作用域中没有找到某个变量时,它咋办?
此时引擎就会在外层嵌套的作用域中继续查找,直到找到该变量,或抵达最外层的作用域为止。此时就形成了 作用域链。
这里说的查找方式就是前面讲的:LHS和 RHS 查询。
LHS 和 RHS 查询都会在当前执行作用域中开始,如果有需要(也就是说它们没有找到所需的标识符),就会向上级作用域继续查找目标标识符,这样每次上升一级作用域,最后到达全局作用域,无论找到或没找到都将停止。
异常处理
- 引擎执行LHS查询时
- 1.1 非 严格模式 下,如果在当前作用域没找到,就会继续往上一层作用域查找,如果在最终全局作用域也没找到。就会 创建一个具有该名称的变量 。
- 1.2 严格模式下面, 和RHS一样,会抛出异常
ReferenceErroe异常。因为 严格模式 禁止自动或隐式地创建全局变量 。
- 引擎执行 RHS 查询时
- 2.1 如果找不到,也是会顺着 作用域网上找,最终没找到的话,会抛出异常
ReferenceErroe异常。 - 2.2 如果找到了,但是你对整个变量进行一个“不合理操作”,也会报错
TypeError.
在努力看书学习中,参考如下,仰望大佬们 ~ 🙆♂️