定义
MDN对闭包的定义为: 闭包是指那些能够访问自由变量的函数
那什么是自有变量呢?自有变量是指在函数中使用的,但既不是函数参数也不是函数的局部变量的变量
由此,我们可以看出闭包共有两部分组成:闭包 = 函数 + 函数能够访问的自由变量
举个例子
var a = 1;
function foo(){
console.log(a);
}
foo();
foo函数可以访问变量a,a对foo来说是自由变量,那么foo就是闭包了。。。
所以在《JavaScript权威指南》中就讲到:从技术的角度讲,所有的JavaScript函数都是闭包。
这怎么跟我们平时认识的闭包不一样呢?
别急,这是理论上的闭包,还有一个时间角度上的闭包。
ECMAScript中,闭包指的是:
1.从理论角度:所有函数,因为他们在创建的时候就将上层上下文的数据保存起来了。
2.从实践角度:一下函数才算闭包:
①即使创建它的上下文已经销毁,它仍然存在(比如,内部函数从父函数中返回)
②在代码中引用了自由变量
接下来讲讲实践上的闭包
分析
让我们写个例子,例子依然是来自《JavaScript权威指南》,稍微做点改动:
var scope = "global scope";
function checkscope(){
var scope = "local scope";
function f(){
return scope;
}
return f;
}
var foo = checkscope();
foo();
首先我们分析一下这段代码中执行上下文栈和执行上下文的变化情况
另一个与这段代码相似的例子,在《深入--执行上下文》中有非常详细的分析
这里直接给出简要的执行过程:
1.进入全局代码,创建全局执行上下文,并压栈
2.全局执行上下文初始化
3.执行checkscope函数,创建chenckscope函数执行上下文,并压栈
4.checkscope函数执行上下文初始化,创建变量对象、作用域链、this等
5.checkscope函数执行完毕,checkscope执行上下文从执行上下文栈中弹出
6.执行f函数,创建f函数执行上下文,并压栈
7.f函数执行上下文初始化,创建变量对象、作用域链、this等
8.f函数执行完毕,f函数执行上下文从栈中弹出
了解到这个过程,我们应该思考一个问题,那就是:
当f函数执行的时候,checkscope函数上下文已经被销毁了啊(即从栈中弹出),怎么还会读取到checkscope作用域下的scope值呢?
当我们了解了具体的执行过程后,我们知道f执行上下文维护了一个作用域链:
fContext = {
Scope:[AO,checkscopeContext.AO,globalContext.VO],
}
就是因为这个作用域链,f函数依然可以读取到checkscopeContext.AO的值,说明当f函数引用了checkscopeContext.AO中的值时,即使checkscopeContext被销毁了,但是js依然会让checkscopeContext.AO活在内存中,f函数依然可以通过作用域链找到他,正是js做到这一点,从而实现了闭包这个概念。
所以,让我们再看一遍实践角度上的闭包的定义:
1.即使创建它的上下文已经销毁,它仍然存在(比如,内部函数从父函数中返回)
2.在代码中引用了自有变量
必刷题
接下来,看这道刷题必刷,面试必考的闭包题
var data = [];
for (var i = 0; i < 3; i++) {
data[i] = function () {
console.log(i);
};
}
data[0]();
data[1]();
data[2]();
答案都是3,让我们分析一下原因
当执行到data[0]函数之前,此时全局上下文的VO为:
globalContext = {
VO:{
data:[...],
i:3
}
}
当执行到data[0]函数的时候,data[0]函数的作用域链为:
data[0]Context = {
Scope:[AO,globalContext.VO]
}
dat[0]Context的AO并没有i值,所以会从globalContext.VO中查找,i为3,所以打印的结果就是3.data[1]和data[2]是一样的道理
所以让我们改成闭包看看:
var data = [];
for (var i = 0; i < 3; i++) {
data[i] = (function (i) {
return function(){
console.log(i);
}
})(i);
}
data[0]();
data[1]();
data[2]();
当执行到data[0]函数之前,此时全局上下文的VO为:
globalContext = {
VO: {
data: [...],
i: 3
}
}
跟没改之前一模一样。
当执行data[0]函数的时候data[0]函数的作用域链发生了改变:
data[0]Context = {
Scope : [AO,匿名函数Context.AO,globalContext.VO]
}
匿名函数执行上下文的AO为:
匿名函数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。
data[1]和data[2]是一样的道理