闭包的定义
MDN 对闭包的定义:
- 闭包: 能够访问自由变量的函数
- 自由变量: 既不是函数参数也不是函数的局部变量
根据上面的例子,举一个例子:
var str = 'xiaoqi';
function getName() {
console.log(str);
}
getName()函数可以返回str这个变量,但str即不是getName函数的局部变量,也不是foo函数的参数,所以str就是自由变量。
这样函数getName就是一个闭包。
是不是觉得跟平时见到的闭包不一样?其实,从技术的角度上看,上面的例子就是一个闭包。
你是否要问:任何函数是否都有闭包,包括在全局范围内创建的函数?答案是肯定的。在全局范围中创建的函数也会创建一个闭包。但由于这些函数是在全局范围内创建的,因此它们可以访问全局范围内的所有变量,就无所谓闭包不闭包了。
实践上的闭包
上面说的是理论上的闭包,现在说一说实践角度的闭包:
我们通常见到的闭包:当内部函数被保存到外部时,才会真正涉及闭包,返回的函数可以访问自由变量。即做到下面两点:
- 即使创建它的上下文已经销毁,它依然存在(比如从父函数返回的子函数)
- 在代码中引用了自由变量
来看一个例子:
var str = 'xiaoqi';
function check() {
var str = 'hhh';
function getName() {
console.log(str);
}
return getName;
}
var foo = check();
foo();
分析一下它的简要执行过程:
首先说明:首先,函数定义可以存储在变量中,函数定义在被调用之前对程序是不可见的
; 其次每次调用函数
时,就会临时创建一个本地执行上下文
,当函数执行结束时,执行上下文就被销毁。
- 进入全局代码,创建全局执行上下文,全局执行上下文压入执行上下文栈
- 全局执行上下文初始化
- 执行check函数,创建check函数执行上下文,check函数执行上下文压入执行上下文栈
- 定义getName函数时,保存了check函数的作用域链
- check函数执行完毕,check函数执行上下文销毁
- 执行getName函数,创建getName函数执行上下文,getName函数执行上下文压入执行上下文栈
- getName函数初始化,创建变量对象等
- getName函数执行完毕,getName函数执行上下文弹出
在getName函数执行时,check函数执行上下文已经被销毁,但因为getName保存了check函数的作用域链, 所以getName函数依然可以读到在check中定义的str(hhh)这个变量。
案例总结:上面的函数getName就是一个实践角度的闭包。其创建它的执行上下文销毁之后,它依然存在,并且它访问自由变量str(既不是getName中参数,也不是其局部变量)。
考题
了解闭包之后,我们来看一看相关考题:
题目1
var data = [];
for (var i = 0; i < 3; i++) {
data[i] = function () {
console.log(i);
};
}
data[0]();
data[1]();
data[2]();
上面提到过,函数在定义时,对程序是不可见的,当for循环执行之后,只会定义data[0]、data[1]、data[2]这些函数。
当执行这些函数时,才会创建执行上下文,会沿着作用域链向上访问,其作用域链是:
Scope: [AO, globalContext.VO]
从全局作用域中访问到i。此时的i已经是3。所以结果都是3。
更改上面的例子,使用立即执行函数解决闭包问题(用闭包解决闭包):
var data = [];
for (var i = 0; i < 3; i++) {
data[i] = (function (x) {
return function () {
console.log(x);
}
})(i);
}
data[0]();
data[1]();
data[2]();
更改之后,执行data[0]、data[1]、data[2]函数时,它们的作用域链:
Scope: [AO, 匿名函数Context.AO globalContext.VO]
所以先会去匿名函数中查找,匿名函数的执行上下文:
匿名函数Context = {
AO: {
arguments: {
0: 0,
length: 1
},
x: 0
}
在data[1]、data[2]中,其匿名执行函数的执行上下文的x分别是1、2。
所以最后的结果就是0、1、2
题目2
写出下面代码的执行过程,用箭头表示其前后的两次输出之间有 1 秒的时间间隔,而逗号表示其前后的两次输出之间的时间间隔可以忽略,代码实际运行的结果该如何描述?
for (var i = 0; i < 5; i++) {
setTimeout(function() {
console.log(new Date, i);
}, 1000);
}
console.log(new Date, i);
这段代码考察了异步、作用域、闭包。
- for循环里设置了5个定时器,定时器是异步执行,循环之后的
console.log(new Date, i);
是同步执行,最先输出的应该就是console.log(new Date, i);
这个同步代码。输出5。 - 同步任务执行之后,开始执行异步任务,而这5个定时器都会在1秒之后触发。所以结果就是
5->5,5,5,5,5
题目2- - -变式1
如果想让上述结果变为5->0,1,2,3,4,
,应该如何改造代码? 出现上面的结果,就是由于闭产生的问题,我们也可以使用立即执行函数解决闭包问题。
for (var i = 0; i < 5; i++) {
(function(i) {
return setTimeout(function() {
console.log(new Date, i);
}, 1000);
})(i);
}
console.log(new Date, i);
增补:有同学可能会想到es6的let ,这样该代码:
for (var i = 0; i < 5; i++) {
setTimeout(function(j) {
console.log(new Date, j);
}, 1000,i);
}
console.log(new Date, i);
使用let代替var,这样i只在本轮循环中有效,但是,最后console.log(new Date, i);输出的i不能访问,let只在自己的块级作用域中有效。
代码最终会报错。
题目2 - - -变式2
如果将代码输出变成0 -> 1 -> 2 -> 3 -> 4 -> 5
,并要求原有代码块中的循环和两处console.log
不变,应该怎么改造代码?
我们可以使用es6中Promise方法。
const tasks = [];
for(var i = 0;i < 5;i++) {
(function (j){
tasks.push(new Promise(function(resolve, reject) {
setTimeout(function(){
console.log(new Date, j);
resolve();
},1000*j)
}))
}(i))
}
Promise.all(tasks).then(function() {
setTimeout(function(){
console.log(new Date, i);
},1000);
})
-
定义一个数组用来存放Promise对象。
-
for循环中每一轮循环都会用立即执行函数push 一个Promise对象。每个promise对象里面有一个定时器,并且每一个定时器的延时时间都乘以相应的j值。
-
将生成的5个Promise对象传给Promise.all(),这个方法会等传入的所有Promise对象状态都变成resolved后,在执行then方法。那么最后的定时器的延时只需要设置为1s。
-
最后结果就是
0 -> 1 -> 2 -> 3 -> 4 -> 5
题目2 - - -变式3
如果要使用async/await特性来更改上面的代码,该如何改?
const sleep = function(delayMs) {
new Promise(function(resolve, reject) {
setTimeout(resolve, delayMs);
})
}
async function asyncPrint() {
for(var i = 0;i < 5; i++){
if(i > 0){
await sleep(1000);
}
console.log(new Date, i);
}
await sleep(1000);
console.log(new Date, i);
};
asyncPrint();
题目3
下面的输出代码是什么:
function fun(n,o){
console.log(o);
return {
fun:function(m){
return fun(m,n);
}
}
}
var a=fun(0); // undefined
a.fun(1);// 0
a.fun(2); // 0
a.fun(3); // 0
var b=fun(0).fun(1).fun(2).fun(3);// undefined 0 1 2
var c=fun(0).fun(1);// undefined 0
c.fun(2);// 1
c.fun(3);// 1
参考文章: