任何JavaScript代码片段在执行前都要进行编译,从编译到执行过程涉及到几个角色:
- 引擎:从头到尾负责整个JavaScript程序的编译及执行过程
- 编译器:负责词法分析、语法分析、代码生成等
- 作用域:负责收集并维护由所有声明的标识符(变量)组成的一系列查询,并实施一套非常严格的规则,确定当前执行的代码对这些标识符的访问权限。
1. 作用域
上面的表述有点绕,简单讲,作用域是用于在运行时代码中的某些特定部分中变量,函数和对象的可访问性。
1.1 全局作用域
全局作用域为最外层的作用域。
js
var name = "global";
console.log(name); // global
console.log(window.name); // global
直接在代码最外层定义一个变量"name",该变量会存储在全局作用域上。在浏览器环境下,全局对象默认为window,在window上访问该标识符效果一致。
1.2 函数作用域
函数作用域的含义是指,属于这个函数的全部变量都可以在整个函数的范围内使用及复用。
js
function foo() {
var age = 25;
console.log(age);
}
foo(); // 25
console.log(age);//ReferenceError:age is not defined
在foo函数中定义了变量age,该变量属于foo的函数作用域中,因此在函数执行时可访问,而在全局作用域中并不存在age变量,因此会报引用错误。
1.3 块作用域
块级作用域由最近的一对包含花括号{} 界定。换句话说, if 块、while 块、function块,单独的块级作用域。
需要使用let
、const
js
function fn(){
var a = 1;
if(a > 0){
var c = 3;
let b = 2;
console.log(b);
}
console.log(c);
console.log(b);
}
fn();
// 2
// 3
// ReferenceError: b is not defined
在函数fn
中,增加if
块,b
使用let
进行声明,故属于if
块构成的块作用域中,而c
使用var
进行声明,无法劫持块作用域,故属于fn
的函数作用域中。
1.4 作用域嵌套
在当前作用域中无法找到某个变量时,引擎就会在外层嵌套的作用域中继续查找,直到找到该变量,或抵达最外层的作用域(也就是全局作用域)为止。
详细在作用域链中讲讲。
2. 预编译
编译阶段发生在代码执行阶段之前,主要有三步:
- 词法分析(词法单元)
- 语法分析(抽象语法树AST)
- 代码生成
编译阶段中的一部分工作就是找到所有的声明,并用合适的作用域将它们关联起来。
2.1 在代码执行之前
2.1.1 变量声明提升
在编译时,将变量的声明 提升到当前作用域 的顶端
js
console.log(a); // undefined
console.log(b); // ReferenceError: b is not defined
var a = 2;
等价于
js
var a;
console.log(a); // undefined
console.log(b); // ReferenceError: b is not defined
a = 2;
可以看到,正是因为声明提升了,默认为undefined
,所以才没有报引用错误。
2.1.2 函数整体提升
变量只会提升声明,而函数会整体进行提升,包括函数体。
函数提升优先级 比 变量提升 更高。
js
foo(); // 1
var foo;
function foo() {
console.log(1);
}
可以看到foo被识别为函数名,且正常执行。
2.2 在函数执行之前
理解这个只需要掌握四部曲
:
(1)创建一个AO(Activation Object)
(2)找形参和变量声明,然后将形参和变量声明作为AO的属性名,属性值为undefined
(3)将实参和形参统一
(4)在函数体内找函数声明,将函数名作为AO对象的属性名,属性值为函数体
js
function fun(a){
var a=1
var a=2
function b(){}
var b=a
a=function c(){}
console.log(a);
c=b
console.log(c);
}
fun(2)
AO的处理过程:
js
// 第一步,创建AO
AO = {};
// 第二步,找形参和变量声明,然后将形参和变量声明作为AO的属性名,属性值为undefined
AO = {
a: undefined,
b: undefined
};
// 第三步,将实参和形参统一
AO = {
a: 2, // 统一
b: undefined
};
// 第四步,在函数体内找函数声明,将函数名作为AO对象的属性名,属性值为函数体
AO = {
a: 2,
b: function(){}
}
进入执行阶段:
js
// 执行var a = 1
AO = {
a: 1,
b: function(){}
}
// 执行 var a = 2
AO = {
a: 2,
b: function(){}
}
// 执行 b = a
AO = {
a: 1,
b: 1
}
// 执行 a=function c(){}
AO = {
a: function c(){}
b: 1
}
// 执行 c=b
// AO上不存在c,引擎会在作用域的上一层即全局作用域上寻找,没找到则会在全局作用域上创建,值为undefined,后被赋值为b
2.3 发生在全局
发生在全局的预编译也有自己的三部曲
:
(1)创建GO(Global Object)对象
(2)找全局变量声明,将变量声明作为GO的属性名,属性值为undefined
(3)在全局找函数声明,将函数名作为GO对象的属性名,属性值赋予函数体
js
global=100
function fn(){
console.log(global);//打印结果:undefined
global=200
console.log(global);//打印结果:200
var global=300
}
fn()
var global
// GO:{
// global:undefined 100
// fn:function fn(){}
// }
// AO:{
// global:undefined, 200
// }
其中GO的创建过程:
js
// 创建GO
GO = {}
// 找全局变量声明,将变量声明作为GO的属性名,属性值为undefined
GO = {
global: undefined
}
// 在全局找函数声明,将函数名作为GO对象的属性名,属性值赋予函数体
GO = {
global: undefined,
fn: function(){...}
}
3. 作用域链
执行期上下文
:当函数执行的时候,会创建一个称为执行期上下文的对象(AO对象),一个执行期上下文定义了一个函数执行时的环境。- 函数每次执行时,对应的执行上下文都是独一无二的,所以多次调用一个函数会导致创建多个执行期上下文,
- 当函数执行完毕,它所产生的执行期上下文会被销毁。
查找变量
:从作用域链的顶端依次往下查找。[[scope]]
:作用域属性,也称为隐式属性,仅支持引擎自己访问。函数作用域,是不可访问的,其中存储了运行期上下文的集合。作用域链
:[[scope]]
中所存储的执行期上下文对象的集合
,这个集合呈链式连接
,我们把这种链式连接叫做作用域链
js
function a(){
var a=1
console.log(a);
}
var glob=100
a()
//a 定义 a.[[scope]]--> 0:GO:{}
//a 执行 a.[[scope]]--> 0:AO:{} 1:GO:{}
当我们定义一个函数 时,这个函数就拥有了scope属性
- 当函数a被定义 时就有了scope属性 ,它记录的是a的执行上下文 ,而函数a中有内容,也就有了全局执行上下文的对象 了,我们把它称为GO(Global Object)。
- 当函数a被执行 时,就带来了函数a自己 的执行上下文的创建,我们把它称为AO (Activation Object)。类似栈的原理,AO来到了GO的位置,GO向后挪了一位。
- 访问变量时,就顺着函数a的作用域链 开始查找,先从AO中寻找,找不到再一层一层向外寻找