从执行上下文深入理解闭包

在学习前端的道路上,对很多知识点我们大多时候都是知其然而不知其所以然,当我们钻牛角尖去研究那些底层原理的时候,我们又容易迷失方向,不禁发出灵魂的拷问——“这个知识究竟有什么用处”,在拜读了某位大佬JS深入学习系列的专题文章后,我不由想要从其底层原理来讲讲闭包的实现过程,以此对闭包有更深入的理解。

1
2
3
4
5
6
7
8
9
10
11
var scope = "global scope";
function checkscope(){
var scope = "local scope";
function f(){
return scope;
}
return f;
}

var foo = checkscope();
foo();

首先,我们以上面这段代码,来看下其执行上下文的构建过程。

执行上下文构建过程

执行全局代码,创建全局执行上下文,全局上下文被压入执行上下文栈。

1
2
3
ECStack = [
globalContext
];

全局上下文初始化。

1
2
3
4
5
globalContext = {
VO: [global, scope, checkscope, foo],
Scope: [globalContext.VO],
this: globalContext.VO
}

初始化的同时,checkscope 函数被创建,保存作用域链到函数的内部属性[[scope]]。

1
2
3
checkscope.[[scope]] = [
globalContext.VO
];

执行 checkscope 函数,创建 checkscope 函数执行上下文,checkscope 函数执行上下文被压入执行上下文栈。

1
2
3
4
ECStack = [
checkscopeContext,
globalContext
];

checkscope 函数执行上下文初始化:

  • 复制函数 [[scope]] 属性创建作用域链,
  • 用 arguments 创建活动对象,
  • 初始化活动对象,即加入形参、函数声明、变量声明,
  • 将活动对象压入 checkscope 作用域链顶端。
  • f 函数被创建,保存作用域链到 f 函数的内部属性[[scope]]
1
2
3
4
5
6
7
8
9
10
11
checkscopeContext = {
AO: {
arguments: {
length: 0
},
scope: undefined,
f: reference to function f(){}
},
Scope: [AO, globalContext.VO],
this: undefined
}

checkscope 函数执行完毕,checkscope 执行上下文从执行上下文栈中弹出

1
2
3
ECStack = [
globalContext
];

执行 f 函数,创建 f 函数执行上下文,f 执行上下文被压入执行上下文栈。

1
2
3
4
ECStack = [
fContext,
globalContext
];

f 函数执行上下文初始化, 以下跟第 4 步相同:

  • 复制函数 [[scope]] 属性创建作用域链
  • 用 arguments 创建活动对象
  • 初始化活动对象,即加入形参、函数声明、变量声明
  • 将活动对象压入 f 作用域链顶端
1
2
3
4
5
6
7
8
9
fContext = {
AO: {
arguments: {
length: 0
}
},
Scope: [AO, checkscopeContext.AO, globalContext.VO],
this: undefined
}

f 函数执行,沿着作用域链查找 scope 值,返回 scope 值。
f 函数执行完毕,f 函数上下文从执行上下文栈中弹出。

1
2
3
ECStack = [
globalContext
];

闭包的定义

现在我们再来看看 ECMAScript 中对闭包的定义。

理论角度

所有的函数。因为它们都在创建的时候就将上层上下文的数据保存起来了。哪怕是简单的全局变量也是如此,因为函数中访问全局变量就相当于是在访问自由变量,这个时候使用最外层的作用域。

实践角度

以下函数才算是闭包:

  • 即使创建它的上下文已经销毁,它仍然存在(比如,内部函数从父函数中返回)。
  • 在代码中引用了自由变量。

所以可见 f 的执行上下文维护了一个作用域链。

1
2
3
fContext = {
Scope: [AO, checkscopeContext.AO, globalContext.VO]
}

在 checkscopeContext 被销毁的情况下,该作用域链依旧被保存了下来,形成了闭包。

经典闭包例子分析

1
2
3
4
5
6
7
8
9
10
11
var data = [];

for (var i = 0; i < 3; i++) {
data[i] = function () {
console.log(i);
};
}

data[0](); //3
data[1](); //3
data[2](); //3

当执行到 data[0] 函数之前,此时全局上下文的 VO 为:

1
2
3
4
5
6
globalContext = {
VO: {
data: [...],
i: 3
}
}

当执行 data[0] 函数的时候,data[0] 函数的作用域链为:

1
2
3
data[0]Context = {
Scope: [AO, globalContext.VO]
}

data[0]Context 的 AO 并没有 i 值,所以会从 globalContext.VO 中查找,i 为 3,所以打印的结果就是 3。

1
2
3
4
5
6
7
8
9
10
11
12
13
var data = [];

for (var i = 0; i < 3; i++) {
data[i] = (function (i) {
return function(){
console.log(i);
}
})(i);
}

data[0](); //0
data[1](); //1
data[2](); //2

当执行到 data[0] 函数之前,此时全局上下文的 VO 为:

1
2
3
4
5
6
globalContext = {
VO: {
data: [...],
i: 3
}
}

当执行 data[0] 函数的时候,data[0] 函数的作用域链发生了改变:

1
2
3
data[0]Context = {
Scope: [AO, 匿名函数Context.AO globalContext.VO]
}

匿名函数执行上下文的 AO 为:

1
2
3
4
5
6
7
8
9
匿名函数Context = {
AO: {
arguments: {
0: 0,
length: 1
},
i: 0
}
}

data[0]Context 的 AO 并没有 i 值,所以会沿着作用域链从匿名函数 Context.AO 中查找,这时候就会找 i 为 0,找到了就不会往 globalContext.VO 中查找了,即使 globalContext.VO 也有 i 的值(值为3),所以打印的结果就是 0。

总结

以上就是对闭包的理解,那么学会了闭包,对我们的日常开发有什么用处呢,闭包在实际应用中最多的,就是用来实现函数柯里化,有关函数柯里化的知识可以参考我的另一篇文章函数柯里化

参考链接:JavaScript深入系列15篇(冴羽)