作用域链精讲
作用域链精讲
- 1编译阶段
- 1.1分词
- 1.2解析(解析为抽象语法树AST)
- 1.3代码生成
- 2执行阶段
- 3查询阶段
- 4嵌套机制(这个比较重要)----就近原则
- 5异常
- 5.1计算机为啥要区分LHS和RHS
- 5.2RHS查询
- 5.3LHS查询
- 6什么是词法作用域
- 7遮蔽效应
- 8变量和函数的声明提升(也是预解析)
- 8.1变量的声明提升
- 8.2函数的声明提升
- 9作用域链和执行环境
- 9.1自由变量
- 9.2上下文执行(执行环境)
- 9.3执行环境栈
- 9.4分析一下整个过程
- 10实例
JavaScript有一套良好的机制用来存储变量,方便变量的查找,这套规则被称作作用域链机制。作用域的内部原理分为编译
、执行
、查询
、嵌套
和异常
5个部分,下面对这5部分进行详细介绍。
1编译阶段
编译过程有3步:分词、解析和代码生成。下面以var a = 2;为例进行这3个过程的说明。
1.1分词
把字符串分解成有意义的代码块,这些代码块被称为词法单元(token)。词法单元组成词法单元流数组。
{
"var":"keyword",//关键字
"a":"indentifier",//标识符
"=":"assignment",//分配
"2":"integer",//整数
";":"eos",//结束语句
}
1.2解析(解析为抽象语法树AST)
把词法单元流数组转换成一个由元素逐级嵌套所组成的代表程序语法结构的树,这个树被称作“抽象语法树”(Abstract Syntax Tree, AST)。
{
operation: "=",
left: {
keyword: "var",
right: "a"
},
right: "2"
}
1.3代码生成
把AST转换成可执行代码(机器指令)的过程称作代码生成。
JS引擎的编译过程其实很复杂,上面的三个是最基本的步骤,任何代码片段在执行前都要先进行编译,这个过程很短,通常是在代码执行前的几微妙完成。
2执行阶段
var a=2;
console.log(a);
console.log(b);
如上,以var a=2;
为例,JS引擎会首先查询作用域,在当前的作用域集合中是否存在一个叫作a的变量。如果是,引擎就会使用这个变量;如果否,引擎会继续查找该变量。
如果最终找到了该变量,就会将值2赋值给当前的变量a;否则引擎就会抛出异常;
3查询阶段
主要两种查询:左侧查询LHS,右侧查询RHS。
可简单理解为变量出现在赋值操作符左边时进行LHS查询,它试图找到变量在内存中的引用地址。当变量出现在赋值操作符的右侧时进行RHS查询,它试图找到一个的具体的数据值。
如下:
function foo(a){
console.log(a)
}
foo(2)
解析一下上面代码的过程:首先第一步进行编译,将所有代码生成计算机可识别的指令(这个不用多说); 然后就开始执行阶段,在执行阶段需要查询(运行foo(2)中需要进行查询)。
- foo()对foo函数对象进行RHS查询
- 函数传参a=2对a进行了LHS引用
- console.log(a)需要对console对象进行RHS查询,并且检查里面有没有log方法
- 接着需要对a进行RHS查询,把得到的值传给了console.log(a);
ps:区分LHS和RHS
console.log(a)
,这里对a的引用是一个RHS引用,因为这里a并没有赋予任何值,我们只是想查找并取得a的值,然后将它打印出来。a=2;
这里对a
的引用是一个LHS
引用,因为我们并不关心当前的值是什么,只是想要为赋值操作找到目标。
简而言之,在这里将RHS理解为取值,LHS理解为找对象。 在这里,也会发现RHS与getter非常类似,LHS与setter相对应。(表达不严谨,只是为了方便初期记忆。)
为了更便于理解,再举个例子:
function foo(a){ // 2. LHS找a的位置,给a赋值2;
var b = a; // 3. RHS找a的值 4. LHS找b的位置,给b赋a的值2;
return a + b; // 5. RHS找a的值 6. RHS找b的值;
};
var c = foo(2) // 1. RHS找foo的值 7. LHS找c的位置,给c赋值foo(2)的值4
4嵌套机制(这个比较重要)----就近原则
作用域链讨论的查找标识符(变量和函数)的问题.当我们使用一个变量时候,js解释器会优先在当前作用域中寻找变量,如果找到了就直接使用;如果没找到就去上一层作用域中去寻找;如果在上一级找到了则使用,没找到就继续去更上一级寻找,依次类推。直到在全局作用域中都没找到,则会报错。
5异常
5.1计算机为啥要区分LHS和RHS
是因为在变量还没有被声明的情况下(在任何作用域中都找不到该变量时),尝试对变量进行LHS和RHS引用会出现不同的错误。
5.2RHS查询
当RHS查询时,如果查询失败,引擎抛出ReferenceError(引用错误)异常:
function foo(a){
a = b;
}
foo();//ReferenceError: b is not defined
如果RHS找到了该变量,但打算对这个变量的值进行不合理的操作(比如试图给一个非函数类型的值进行函数调用,或者引用null或undifined类型的值中的属性),那么引擎或抛出另一种类型的异常,叫做TypeError。
ReferenceError同作用域判别失败相关,而 TypeError 则代表作用域判别成功了,但是对结果的操作是非法或不合理的。
5.3LHS查询
当引擎执行LHS查询时,如果在全局作用域中都没有查找到该变量,那么引擎就会要求全局作用域创建一个新的变量,并让作用域将这个变量返还给自己。前提是程序运行在非“严格模式”下。“严格模式”下,不成功的LHS引用也会抛出ReferenceError异常。
// 示例1
function foo(){
a = 1;
}
foo();
console.log(a);//1
// 示例2
function foo(){
'use strict';
a = 1;
}
foo();
console.log(a);//ReferenceError: a is not defined
6什么是词法作用域
作用域有两种—动态作用域和词法作用域。js是词法作用域。
ps:词法作用域和动态作用域分别是什么
- 词法作用域:函数定义时,即确定的作用域。js中的作用域链,在函数声明时候,就已经确定了,无论函数在何处调用,其作用域变量的查找都是按照定义的包含关系去查找的。
- 动态作用域:变量的作用域与函数的调用地点有关,在不同的函数中调用,变量的查找会沿着调用函数向上查找。
如图,在全局作用域(区域1)声明的变量只有foo;
在foo作用域(区域2)内声明的变量有a,b,bar;
在bar作用域(区域3)内声明的变量有c;
再举个例子:
var a = 2;
function foo() {
console.log(a);
}
function bar() {
var a = 3;
foo();
}
bar();
在词法作用域,a的值会先在foo中查找,没有的话到全局中查找,所以a=2。
如果在动态作用域中,a的值先在foo中查找,没有的话到其调用函数中查找,即bar中查找,a=3。
总结:词法作用域是一种在编写代码时确定的静态作用域规则。变量的作用域是在代码编写阶段确定的,而不是在运行时根据程序的流程信息动态确定的。
7遮蔽效应
JavaScript函数的遮蔽效应(或称作变量屏蔽)是指在函数作用域内定义的变量会隐藏(或遮蔽)同名的全局变量。换句话说,当使用函数作用域内的变量时,会优先使用函数作用域内的变量,而不是全局变量。
var x = 10;
function foo() {
var x = 20;
console.log(x); // 输出20
}
foo();
console.log(x); // 输出10,不是20,要特别注意。因为foo中的x这个变量是在foo作用域内的
8变量和函数的声明提升(也是预解析)
8.1变量的声明提升
提升的是声明而不是赋值!!!使用var声明的变量,会在所有代码执行前被声明(没赋值)。
console.log(b)//会打印undefined,不会报错
var b=2;
8.2函数的声明提升
使用函数声明(function关键字)创建的函数,会在所有代码执行前被创建,由此可在函数声明之前调用函数。(用表达式声明的函数则不行)。
思考:在javascript中当使用var声明变量和function声明函数变量名发生冲突时,那个优先级会更高?
函数声明的优先级高于变量声明,即使变量声明在函数声明之前编写。
9作用域链和执行环境
9.1自由变量
在当前作用域中存在但未在当前作用域中声明的变量。一旦有自由变量,就一定存在作用域链,我们需要根据作用域链的查找机制来查找该自由变量。
9.2上下文执行(执行环境)
每个执行环境都有一个与之关联的变量对象,在环境中定义的函数和变量都保存在这个对象中。如下:
var a=1;
var b=2;
function fn(x){
var a=10;
function bar(x){
var a=100;
b=x+a;//这个b就是自由变量
return b;
}
bar(20);
bar(200);
}
fn(0)
如上:
f(0)的执行环境是:
x:0
a:undefiend(这里a会变量提升)
bar:function
arguments:[0]//没有执行,所以这时候是0个参数
this:window
bar(20)的执行环境是:
x:20
a:undefiend(这里a也会变量提升)
arguments:[0]//没有执行,所以这时候是0个参数
this:window
没有b,b是自由变量
bar(200)的执行环境是:
x:200
a:undefiend(这里a也会变量提升)
arguments:[0]//没有执行,所以这时候是0个参数
this:window
没有b,b是自由变量
9.3执行环境栈
其实就是压栈和出栈的过程。
var a=1;
var b=2;
function fn(x){
var a=10;
function bar(x){
var a=100;
b=x+a;//这个b就是自由变量
return b;
}
bar(20);
bar(200);
}
fn(0)
如上代码,我们只分析执行bar(20)那一刻的执行环境栈,如下:
我们会发现这一时刻,这个执行环境栈中有三个执行环境----全局执行环境,fn(0)执行环境,bar(20)执行环境。其中bar(20)执行环境处于活跃状态。
此时bar(20)执行环境处于活跃状态。fn(0)和全局执行环境已经是非活跃状态(已经执行过了)。所以fn(0)执行环境中a此时为10,全局执行环境中此刻a为1,b为2。
9.4分析一下整个过程
如上图,分析一下整个过程。
一开始栈中只有全局执行环境:
执行完第2行代码后,全局执行环境下的a和b已经赋值了:
执行完第13行代码后,执行环境栈中新增了fn(0)执行环境(此时fn(0)执行环境处于活跃状态):
执行完第13行代码后,fn(0)执行环境中的a被赋值为10;
执行完第10行代码后,执行环境栈中新增了bar(20)执行环境(此时bar(20)执行环境处于活跃状态):
执行完第8行代码后,bar(20)执行环境中的a被赋值为100,全局中的b被赋值为120
执行完第9行代码后,bar(20)执行环境要出栈(此时fn(0)执行环境处于活跃状态):
执行完第11行代码后,执行环境栈中新增了bar(200)执行环境(此时bar(200)执行环境处于活跃状态),注意此时全局中的b已经是120了:
执行完第8行代码后,bar(200)执行环境中的a被赋值为100,全局中的b被赋值为300
执行完第9行代码后,bar(200)执行环境要出栈(此时fn(0)执行环境处于活跃状态):
执行完第12行代码后,fn(0)执行环境要出栈(此时全局执行环境处于活跃状态)
执行完毕后,全局执行环境也会出栈:
10实例
实例1:
var a = 1;
function fn1() {
a = 2;//a是自由变量
console.log(a)//2
}
fn1();
console.log(a)//2
实例2:
var b = 1;
function fn2() {
var b = 2;
console.log(b)//2
}
fn2();
console.log(b)//1,这里要注意与1的区别,此时fn2()执行环境已经出栈了
实例3:
//定义形参,相当于在函数中声明对应的变量
var c=1;
function fn3(c){
console.log(c);//undefined
c=2;
console.log(c);//2
}
fn3()
console.log(c)//1,这里要注意与1的区别,此时fn2()执行环境已经出栈了
实例4:
var d=1;
function fn4(d){
console.log(d);//10
d=2;
console.log(d);//2
}
fn4(10)
console.log(d)//1
实例5:
var e=1;
function fn5(e){
console.log(e);//1
e=2;
console.log(e);//2
}
fn5(e)
console.log(e)//1
实例6:
console.log(f);//function{alert(2)}
var f=1;
console.log(f);//1
function f(){
alert(2)
}
console.log(f)//1
var f=3;
console.log(f);//3
var f=function (){
alert(4)
}
console.log(f)//function{alert(4)}
var f;
console.log(f)//function{alert(4)}