前言
陆陆续续看过一些对js闭包(closure),发现对闭包理解一直差点意思。直到学习了js预编译,才恍然大悟,原来从预编译出发看闭包,才能看得更加透彻!
预备知识
首先,我们得先对预编译进行一些简要地介绍。
执行上下文
执行上下文可理解为当前的执行环境,与该运行环境相对应。js引擎每进入一个环境就会创建相应的执行上下文,创建执行上下文的过程中,主要做了以下三件事件,如图:
预编译
大家都知道,javascript是解释性语言,主要特点为解释一行执行一行。
而在js的执行分为3个阶段
- 语法分析:script标签加载即开始语法分析,分析整个标签内的语法错误。无错误即进入预编译阶段。
- 预编译: 每进入一个新环境,就进行一次预编译,同时创建一个执行上下文并放入执行栈中。此时,当前环境会进行一定的函数声明提升和变量声明提升,注意:函数声明提升优先于变量声明提升。环境中没有新的函数声明则进入解释执行阶段。
- 解释执行:将当前执行上下文中的VO->AO, 此后,js引擎在当前环境从上到下、从左到右执行代码,不断改变AO中的变量等内容。当前上下文执行完毕则出栈,执行下一个上下文。
要点:js进入全局环境时,主线程创建全局执行上下文,全局执行上下文中的变量对象VO(globalContext) === global,或称为GO;在进入函数环境时,会创建执行上下文,并放入执行栈,而在函数执行时执行上下文中的VO->AO
预编译四部曲:
- 创建AO(Activation object)对象
- 找形参和变量声明,将变量声明的名(即变量和形参名)作为AO属性名,值为undefined
- 将实参和形参统一
- 在函数体里面找函数声明,值赋予函数体(注意此处的函数声明要区别于函数表达式)
预编译例子分析
1
2
3
4
5
6
7
8
9
10
11
12//例题
function fn(a){
console.log(a);
var a = 123;
console.log(a);
function a() {}
console.log(a);
var b = function() {};
console.log(b);
function d() {}
}
fn(1); - 创建AO(Activation object)对象
1
AO = {}
- 找形参和变量声明,将变量声明的名(即变量和形参名)作为AO属性名,值为undefined
1
2
3
4AO = {
a:undefined,//形参
b:undefined,//变量声明
} - 将实参和形参统一
1
2
3
4AO = {
a:1,//将实参值赋给形参
b:undefined,//变量声明
} - 在函数体里面找函数声明,值赋予函数体为了更加直观地表述这个过程,我们上述代码写成下面的等价形式
1
2
3
4
5AO = {
a:function a(){},
b:undefined,//变量声明
d:function d(){},
}由上述代码,我们可以很简单地分析出打印结果为:1
2
3
4
5
6
7
8
9
10
11
12
13//例题
function fn(a){
function a() {}
function d() {}
//var a;函数声明优先于变量声明
console.log(a);
a = 123;
console.log(a);
console.log(a);
var b = function() {};//注意函数表达式不提升
console.log(b);
}
fn(1);
ƒ a() {}
123
123
ƒ () {}
真正的预编译
1 | //例题: |
刚进入全局环境时预编译
1 | VO(globalContext) = { |
或直接写成
1 | GO = { |
通俗来说GO比起AO只是少了实参和形参统一的步骤。
当进入函数test环境时,js预编译创建执行上下文并入执行栈,在函数执行时VO->AO。初始AO如下所示。
1 | AO(test) = { |
闭包实例分析
有了上面的预备知识,我们将举几个经典的闭包例子,从预编译出发来进行分析。
例子一
1 | const foo = (function() { |
- 创建全局执行上下文,此时GO对象为
1
2GO = {
} - 执行完第一条赋值语句后,此时GO对象为
1
2
3
4
5GO = {
foo:() => {
return v++
}
}