本文已参与「新人创作礼」活动,一起开启掘金创作之路
作用域
先来引入这个概念:
为什么要有作用域?
编程语言最基本的功能:就是要能够储存变量当中的值,并且能在之后对这个值进行访问或修改。
所以作用域这个概念就引申出来了,我们设计了一套规则来存储变量,而这套规则就被称为作用域。
编译原理
下面我们来了解下JavaScript是怎样编译程序的,有助于我们后面对作用域的理解。
程序在执行前,需要被编译,编译又分为三个阶段,分别是:词法分析、语法分析、代码生成
词法分析
也叫分词(Tokenizing),这么说吧,为什么又会有这样的叫法呢
简单来说,区别就在于词法单元的识别是否是有状态的
有状态的识别称为词法分析,无状态的识别称为分词
var b = 13会被分解为
[ { "type": "Keyword", "value": "var" }, { "type": "Identifier", "value": "b" }, { "type": "Punctuator", "value": "=" }, { "type": "Numeric", "value": "13" }, { "type": "Punctuator", "value": ";" }]
Keyword(关键词) |
Identifier(标识符) | Punctuator(标点符号) | Numeric(数字)
最终会被解析为词法单元流,以数组的形式
语法分析
也叫解析(Parsing),这个过程是一个:数组 => 树 的过程
树是抽象语法树(Abstract Syntax Tree)
{
"type": "Program",
"body": [
{
"type": "VariableDeclaration",
"declarations": [
{
"type": "VariableDeclarator",
"id": {
"type": "Identifier",
"name": "b"
},
"init": {
"type": "Literal",
"value": 13,
"raw": "13"
}
}
],
"kind": "var"
}
],
"sourceType": "script"
}
代码生成
接下来需要将AST转化为可执行代码
Javascript引擎和编译器和作用域
上面讲了在执行代码前需要编译,中间会声明变量
那么这些变量就是储存在作用域中(scope)
作用域又分为函数作用域和全局作用域
所以我们要运行一段JavaScript代码时,JavaScript引擎会通过查找作用域中的变量来执行代码。
编译器通过三个步骤:分词、解析、生成代码
然后引擎执行代码,通过作用域的协助,进行LHS查询和RHS查询
这里不具体解释,通过例子来理解这两个查询
下面来看个例子帮助了解
function foo(a) {
console.log( a )
}
foo( 2 );
当执行以下代码的时候,(在执行前很短很短的时间,JavaScript编译器才将这段代码编译好,生成执行代码)
引擎先在全局作用域中查看是否有函数foo的声明,进行RHS查询,
foo(2)中的2,相当于a = 2,要给a赋值上2,需要进行LHS查询,
然后进入到函数foo内部,console.log 对console对象进行RHS查询
再对a进行RHS查询
每次引擎进行查询(对变量、对象、函数),都是看作用域中是否有声明
作用域
JavaScript有两种工作模式:
- 词法作用域
- 动态作用域
JavaScript 支持词法作用域,在树状嵌套结构中代码块创建出新的作用域。
// global scope
function scopeOne() {
// scope 1
function scopeTwo() {
// scope 2
}
}
在 JavaScript 中,每当你创建了一个引用,不管是通过变量(variable)、函数(function)、类型(class)、参数(params)、模块导入(import)还是标签(label)等,它都属于当前作用域。
var global = "I am in the global scope";
function scopeOne() {
var one = "I am in the scope created by `scopeOne()`";
function scopeTwo() {
var two = "I am in the scope created by `scopeTwo()`";
}
}
更深的作用域可以引用外部作用域的声明的变量
function scopeOne() {
var one = "I am in the scope created by `scopeOne()`";
function scopeTwo() {
one = "I am updating the reference in `scopeOne` inside `scopeTwo`";
}
}
内层作用域也可以创建和外层作用域同名的引用
function scopeOne() {
var one = "I am in the scope created by `scopeOne()`";
function scopeTwo() {
var one = "I am creating a new `one` but leaving reference in `scopeOne()` alone.";
}
}
根据上面的例子,我们时常可以看到一个函数或一个块嵌套在另一个函数或者块当中
如果引擎在当前作用域不能找到变量,就会往外层作用域去查找
引擎会先在当前作用域去查找,LHS和RHS本质上都可以理解为:找变量。
对一个还没有声明的变量而言,也就是说:在任何作用域都找不到
以下两个例子都是基于还没有声明的变量来说的
进行RHS查询时,会产生ReferenceError的异常
进行LHS查询时,却并不会产生异常,而是会返回undefined(非严格模式下)
(当然这里的b输出是没问题的为2)
小结
LHS查询是:查找的目的是对变量进行赋值
RHS查询是:查找的目的是获取变量的值
编译阶段中的一部分工作就是找到所有的声明,并用合适的作用域将它们关联起来
参考文章:
你不了解的JavaScript