前言

陆陆续续看过一些对js闭包(closure),发现对闭包理解一直差点意思。直到学习了js预编译,才恍然大悟,原来从预编译出发看闭包,才能看得更加透彻!

预备知识

首先,我们得先对预编译进行一些简要地介绍。

执行上下文

执行上下文可理解为当前的执行环境,与该运行环境相对应。js引擎每进入一个环境就会创建相应的执行上下文,创建执行上下文的过程中,主要做了以下三件事件,如图:
avatar

预编译

大家都知道,javascript是解释性语言,主要特点为解释一行执行一行。
而在js的执行分为3个阶段

  1. 语法分析:script标签加载即开始语法分析,分析整个标签内的语法错误。无错误即进入预编译阶段。
  2. 预编译: 每进入一个新环境,就进行一次预编译,同时创建一个执行上下文并放入执行栈中。此时,当前环境会进行一定的函数声明提升和变量声明提升,注意:函数声明提升优先于变量声明提升。环境中没有新的函数声明则进入解释执行阶段。
  3. 解释执行:将当前执行上下文中的VO->AO, 此后,js引擎在当前环境从上到下、从左到右执行代码,不断改变AO中的变量等内容。当前上下文执行完毕则出栈,执行下一个上下文。

要点:js进入全局环境时,主线程创建全局执行上下文,全局执行上下文中的变量对象VO(globalContext) === global,或称为GO;在进入函数环境时,会创建执行上下文,并放入执行栈,而在函数执行时执行上下文中的VO->AO

预编译四部曲:

  1. 创建AO(Activation object)对象
  2. 找形参和变量声明,将变量声明的名(即变量和形参名)作为AO属性名,值为undefined
  3. 将实参和形参统一
  4. 在函数体里面找函数声明,值赋予函数体(注意此处的函数声明要区别于函数表达式)

    预编译例子分析

    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);
  5. 创建AO(Activation object)对象
    1
    AO = {}
  6. 找形参和变量声明,将变量声明的名(即变量和形参名)作为AO属性名,值为undefined
    1
    2
    3
    4
    AO = {
    a:undefined,//形参
    b:undefined,//变量声明
    }
  7. 将实参和形参统一
    1
    2
    3
    4
    AO = {
    a:1,//将实参值赋给形参
    b:undefined,//变量声明
    }
  8. 在函数体里面找函数声明,值赋予函数体
    1
    2
    3
    4
    5
    AO = {
    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
2
3
4
5
6
7
8
9
10
11
12
13
14
//例题:
function test(){
console.log(b);//undefined;
if (a) {
var b = 100;
}
console.log(b);//undefined
c = 234;
console.log(c); //234;
}
var a;
test();
a = 10;
console.log(c); //234;

刚进入全局环境时预编译

1
2
3
4
5
VO(globalContext) = {
a:undefined,
c:undefined,//非严格模式
test:function(){...},//这里略写了函数体
}

或直接写成

1
2
3
4
5
GO = {
a:undefined,
c:undefined,//非严格模式
test:function(){...},//这里略写了函数体
}

通俗来说GO比起AO只是少了实参和形参统一的步骤。
当进入函数test环境时,js预编译创建执行上下文并入执行栈,在函数执行时VO->AO。初始AO如下所示。

1
2
3
AO(test) = {
b:undefined,
}

闭包实例分析

有了上面的预备知识,我们将举几个经典的闭包例子,从预编译出发来进行分析。

例子一

1
2
3
4
5
6
7
8
9
10
11
12
const foo = (function() {
var v = 0
return () => {
return v++
}
}())

for (let i = 0; i < 10; i++) {
foo()
}

console.log(foo())
  1. 创建全局执行上下文,此时GO对象为
    1
    2
    GO = {    
    }
  2. 执行完第一条赋值语句后,此时GO对象为
    1
    2
    3
    4
    5
    GO = {
    foo:() => {
    return v++
    }
    }

后记