MDN对闭包的定义为
闭包是指那些能够访问自由变量的函数
那什么是自由变量呢
自由变量是指在函数中使用的,但既不是函数参数也不是函数的局部变量的变量,
由此,我们可以看出闭包共有两部分组成
闭包= 函数+函数能够访问的的自由变量
举个例子
var a = 1;
function foo() {
console.log(a);
}
foo();
foo函数可以访问变量a ,但是a既不是foo函数的局部变量,也不是foo函数的参数,所以a是自由变量
那么,函数foo+foo函数访问的自由变量啊不就是构成了一个闭包吗。。。。
还真是如此
‘
所以在《javascript权威指南》中就有讲过,从技术的角度来讲,所有JavaScript函数都是闭包
很奇怪,这好像跟我们平时讲的闭包不一样了?
别着急,这是理论上的闭包,其实还有一个实践角度上的闭包,让我们看看汤姆大叔的翻译关于闭包的文章中的定义:
ECMAScript中,闭包是指:
1:从理论角度:所有的函数,因为他们创建的时候就将上层上下文的数据保存起来了,哪怕是简单的全局变量也是如此,因为函数中访问全局变量就相当于访问自由变量,这个时候使用最外层的作用域。
2:从实践的角度,以下函数才算是闭包。:
i 。即使创建他的上下文已经销毁,他仍然存在(比如,内部函数从父函数中返回)
ii。在代码中引用了自由变量
接下来就讲讲实践上的闭包。
分析
让我们先写个例子,例子仍然是来自《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函数,创建checkscope函数执行上下文,checkscope执行上下文被压入执行上下文栈
4,checkscope执行上下文初始化,创建变量对象,作用域链,this等
5.。checkscope函数执行完毕,checkscope执行上下文从执行上下文栈中弹出
6。执行 f函数,创建f函数执行上下文,f执行上下文被压入执行上下文栈
7,。f执行上下文初始化,创建变量对象,作用域连,this等。
8。f函数执行完毕f函数上下文从执行上下文栈弹出
了解到这个过程,我们应该思考一个问题,就是:
当f函数执行的时候,checkscope函数上下文已经被销毁了。(即从执行上下文栈中弹出),怎么还会读取到checkscope作用域下的scope值呢?
以上的代码,要是转换成PHP,就会报错,因为在PHP中,f 函数只能读取到自己作用域里的值,所以读不到checkscope 下的scope 的值,
然而JavaScript 却是可以的!
当我们了解了具体执行过程后,我们知道 f 执行上下文维护了一个作用域链:
fContext = {
Scope: [AO, checkscopeContext.AO, globalContext.VO],
}
对的,就是因为这个作用域链,f 函数依然可以读取到checkscopeContext.AO的值,说明当 f 函数引用了checkscopeContext.AO中的值的时候,即使checkscopeContext.AO被销毁了,但是JavaScript依然会让checkscopeContext.AO活在内存中去,f函数仍然可以通过f 函数作用链找到他,正式因为JavaScript 做到了这一点,从而实现了闭包的概念,
所以,让我们再看一遍实践角度上闭包的定义:
即使创建它的上下文已经销毁,它仍然存在(比如,内部函数从父函数中返回)
在代码中引用了自由变量
在这里再补充一个《JavaScript权威指南》英文原版对闭包的定义:
This combination of a function object and a scope (a set of variable bindings) in which the function’s variables are resolved is called a closure in the computer science literature.
闭包在计算机科学中也只是一个普通的概念,大家不要去想得太复杂。
必刷题
接下来,看这道题必刷,面试必考的闭包题
var data = [];
for (var i = 0; i < 3; i++) {
data[i] = function () {
console.log(i);
};
}
data[0]();
data[1]();
data[2]();
答案都是三,让我们分析一下原因
当执行到data【0】函数之前,此时全局上下文的vO为
globalContext = {
VO: {
data: [...],
i: 3
}
}
当执行data[0]的时候,data[0]函数的作用域链为:
data[0]Context = {
Scope: [AO, globalContext.VO]
}
data[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]
}
匿名函数Context.AO执行上下文的AO为
匿名函数Context = {
AO: {
arguments: {
0: 0,
length: 1
},
i: 0
}
}
data[0]Context的AO并没有i值。所以会沿着作用链从匿名函数Context.AO中查找,这时候会找i为0 找到了就不会往VO中查找,即使globalContext。VO也有i的值,所以打印的结果就是0
同理 ,data[1]和data[2]也是一样的、、、、、、、、、、、、、、、、、、、、、