理解JS作用域笔记(一):五个阶段、词法作用域
# 开头
笔记视频内容源自B站JavaScript从入门到放弃 第九章 深入理解JS作用域 (opens new window),笔记为自行整理复习使用,欢迎一同学习交流,转载请通知原作者
# 一、内部原理
JS中的作用域分为两个作用域,一个是全局作用域
,一个是函数作用域
,JS引擎有着一套良好的规则去存储变量,并能够很方便的去找到变量,举一个例子:
<body>
<script>
var a = 2;
console.log(a);
function add() {
var b = 3;
console.log(a);
}
console.log(b);
</script>
</body>
2
3
4
5
6
7
8
9
10
11
a变量作为全局作用域
,它能够在函数下被找到,这套规则就叫做作用域
,b作为函数作用域
下的变量无法在函数外被找到。理解好作用域也对后续理解this
有做帮助。JS代码没有编译阶段,是一种边解释边执行的语言。
作用域内部可以划分为五个阶段(了解):
- 编译
- 执行
- 查询
- 嵌套
- 异常
# 1.1、编译阶段
我们以一个代码为例子:
<script>
var a = 2;
</script>
2
3
我们将编译阶段划分成三个步骤:分词(分成不同的词法单元)、解析、代码生成
分词首先将语句分为一个个的词法单元:
var , a , = , 2, ;
在解释阶段内部将其放入一个数组或对象:
{
"var":"keyword",//关键字
"a":"indentifier",//标识符
"=":"assignment",//分配
"2":"interger",//整数
";":"eos",//(end of statement)结束语句
}
2
3
4
5
6
7
解析功能将对应的属性转化为一个抽象语法树
(AST Abstract Snatax Tree)
代码生成将抽象语法树转化为可执行的代码的过程,转化成一组机器指令,我们称这个过程为代码生成
# 1.2、执行阶段
代码生成后就来到了执行阶段,JS是边解释边执行,以一段代码举例子:
<script>
var a = 2;
console.log(a);
console.log(b);
</script>
2
3
4
5
1、JS引擎执行时首先会查找作用域,首先查找a是否在当前作用域
下,如果是,引擎就会直接使用这个变量。如果不在当前作用域则会继续往外找,直到找不到为止
2、如果找到了变量,就会将 2赋值给当前的这个变量,否则引擎就会抛出异常,就像代码示例中的b未定义,控制台抛出了异常
# 1.3、查询阶段
先看代码:
<body>
<script>
var a = 2;
</script>
</body>
2
3
4
5
查询阶段,引擎查询a变量,这样的查询叫做LHS(Left hand Side)
左查询,RHS(Right hand Side)
右查询用在函数的调用,比如:
<script>
var a = 2;
function add(){
}
add();
</script>
2
3
4
5
6
当变量出现在赋值操作的左侧时,查询为LHS
,出现在右侧时如函数调用就为RHS
看一个例子:
function foo(a) {
console.log(a);
}
foo(2);
2
3
4
1、foo(2)
对function函数对象做了一个RHS引用
,这种引用是一种查询引用
2、函数传参a=2
对a变量进行了LHS引用
3、console.log(a);
对console对象进行RHS引用
,并检查console中是否有log方法
4、console.log(a);
对a进行RHS引用
,并把得到的结果传给了console.log(a);
# 1.4、嵌套阶段
嵌套阶段是一个非常重要的阶段,嵌套机制是根据当前作用域进行查找的机制,先看代码:
<body>
<script>
//作用域变量的查找机制(重要)
//在当前作用域下无法找到某个变量时,引擎就会在外层嵌套的作用域中继续查找,直到找到该变量或者是抵达最外层作用域(全局作用域)为止
function foo(a) {
console.log(a + b);
}
var b = 2;
foo(4);
</script>
</body>
2
3
4
5
6
7
8
9
10
11
JS引擎在执行这句话时,经历了前三个阶段后,在当前的这个作用域下去寻找b,找不到的话会向外层(全局嵌套的作用域)寻找b变量。
这里使用到了变量提升
,js引擎会判断var变量
,将其提升到最前面,所以不用去在意变量声明的位置
<body>
<script>
//作用域变量的查找机制(重要)
function foo(a) {
console.log(a + b);
function fo() {
console.log(a + b);
}
}
var b = 2;
foo(4);
</script>
</body>
2
3
4
5
6
7
8
9
10
11
12
13
这个代码同样的思路,无论嵌套多深,寻找作用域都是从内而外去寻找,直到全局作用域下没发现位置,报错
# 1.5、异常阶段
异常就是在浏览器上出现的一些错误
例子1:
<body>
<script>
function fn(a){
a = b;
}
fn(2);
</script>
</body>
2
3
4
5
6
7
8
我们能发现b is not defined
,因为b是一个未声明的变量,在当前作用域的RHS查询时未找到,在全局作用域也找不到,所以输出了错误
例子2:
function fn2(){
var b = 0;
b();
}
2
3
4
例子3:
function fn(){
a = 1;
}
fn();
console.log(a);
2
3
4
5
如果fn()这一行被注释掉就会引起报错:a is not defined
,加上这一行则不会报错,这是因为执行函数时,引擎执行了LHS查询,将1赋值给了a变量,如果在该函数中没有声明a这个变量,引擎将会在全局作用域中
声明一个a变量并进行赋值操作,所以外层作用域下的log
方法能够访问到a这个变量
但是在严格模式下就无法进行这种操作:
function fn(){
'use strict';
a = 1;
}
fn();
console.log(a);
2
3
4
5
6
所以一般在使用函数时都会加上这句话,防止一些非法操作
# 二、词法作用域
# 2.1、作用域查找机制
作用域分为两种,一个是词法作用域
,一个是动态作用域
,JS中不存在所谓的动态作用域,所以我们通过一个案例来看看词法作用域
<body>
<script>
function foo(a) {
var b = a * 2;
function bar(c) {
console.log(a, b, c);
}
bar(b * 3);
}
foo(2);
</script>
</body>
2
3
4
5
6
7
8
9
10
11
12
首先外层有一个作用域,foo函数
有一个函数作用域,bar
也有一个作用域,有三层作用域。从作用域的角度分析,我们将代码分为三层作用域,最外层全局作用域1,foo函数作用域2,bar函数作用域3,1包含2包含3
首先1全局作用域下找到了一个foo(词),在2的作用域下找到了b、a、bar(三个词),3下只有一个c(词)
执行foo时,2赋值给a,a变量是属于foo作用域的,foo作用域内有找到a就赋值给它,bar函数把b*3的值赋值给c,来到bar的作用域,发现a并不在当前bar函数的作用域下,于是去它的外层作用域(foo)查找,找到了a,再去找b,b不在当前作用域于是往外层(foo)寻找。最后再去找c,c在当前作用域下(bar)就赋值。也就是说作用域是由代码里函数声明时决定的,你在哪里声明,他所在的执行上下文就被决定了,通过词法作用域才可以预测代码在执行过程中如何查找标识符。
# 2.2、遮蔽效应
什么是遮蔽,作用域查找从运行时所处的最内部作用域开始,逐级向上
进行,直到遇到第一个匹配的标识符为止
在多层的嵌套作用域中可以定义同名字的标识符,这叫做遮蔽效应
<body>
<script>
var a = 0;
function test() {
var a = 1;
console.log(a);
}
test();
</script>
</body>
2
3
4
5
6
7
8
9
10
在进入test函数作用域时首先查找a变量是否在当前作用域,发现第一个匹配
的就不会再去向外匹配查询,这样内层作用域就覆盖了外层的变量,这就是遮蔽效应
所以全局声明的变量要小心函数中再去声明相同的变量,以免遮蔽效应的产生。