JavaScript基础专题之闭包(四)
定义
MDN 对闭包的定义为:
闭包是指那些能够访问自由变量的函数。
什么又是自由变量呢?
自由变量是指在函数中使用的,但既不是函数参数也不是函数的局部变量的变量。
举个例子:
var a = 0; //自由变量
function foo() {
console.log(a);//访问自由变量,此时这个变量并不是函数参数或者函数的局部变量
}
foo();
foo 函数可以访问变量 a,但是 a 既不是 foo 函数的局部变量,也不是 foo 函数的参数,所以我们说 a 就是自由变量,那么函数 foo 就形成了一个闭包。
所以在《 JavaScript权威指南 》中讲到:从技术的角度讲,所有的 JavaScript 函数都是闭包。
在ECMAScript中,闭包指的是:
- 从理论角度:所有的函数。因为它们都在创建的时候就将上层上下文的数据保存起来了。哪怕是简单的全局变量也是如此,因为函数中访问全局变量就相当于是在访问自由变量,这个时候使用最外层的作用域。
- 从实践角度:以下函数才算是闭包:
- 即使创建它的上下文已经销毁,它仍然存在(比如,内部函数从父函数中返回)
- 在代码中引用了自由变量
接下来就来讲讲实践上的闭包。
常见的闭包问题
以下代码为什么与预想的输出不符?
// 代码1
for (var i = 0; i < 5; i++) {
setTimeout(() => {
console.log(i) // 输出5次5
}, 0)
}
假设A:因为 setTimeout
这块的任务直接进入了事件队列中,所以 i
循环之后i先变成了5,再执行 setTimeout
, setTimeout
中的箭头函数会保存对i的引用,所以会打印5个5.
// 代码2
for (let i = 0; i < 5; i++) {
setTimeout(() => {
console.log(i) // 输出 0,1,2,3,4
}, 0)
}
假设结论 A 成立,那么上式应该也是输出5次5,但是很明显不是,所以结论A并不完全正确。
那我们去掉循环,先写成最简单的异步代码:
function test(a){
setTimeout(function timer(){
console.log(a)
},0)
}
test('hello')
复制代码执行 test
,setTimeout
将 timer
函数放入了事件队列,timer
保留着 test
函数的作用域(在函数定义时创建的),test
执行完毕,主线程上没有其他任务了,timer
从事件队列中出队,执行 timer
,执行 console.log ( a )
,由于闭包的原因,a 依然会保留着之前的引用,输出 'hello'
。
那我们在回到题目中,因为两段代码中的不同只有声明语句,所以我们提出假设B
:因为在代码1
中,匿名函数保留着外部词法作用域,i
都是在全局作用域上,代码2
中由于存在块作用域,所以它保留着每次循环时i的引用。
// 代码3
for (var i = 0; i < 5; i++) {
((i) => {
setTimeout(function timer() {
console.log(i) // 输出 0,1,2,3,4
}, 0)
})(i)
}
复制代码使用 IIFE
传递了变量i给匿名函数,IIFE
产生了一个新作用域,timer
中保留对匿名函数中的i的引用,所以会依次输出。
// 代码4
for (var i = 0; i < 5; i++) {
(() => {
setTimeout(function timer() {
console.log(i) // 输出 5个5
}, 0)
})()
}
和代码3
的区别为IIFE
没有给匿名函数传递 i,timer
保留的作用域链中对i的引用还是在全局作用域上。
经过以上两个变体的验证,所以假设B
成立,即:由于作用域链的变化,闭包中保留的参数引用也发生了变化,输出的参数也发生了变化。
下例,循环中的每个迭代器在运行时都会给自己捕获一个i
的副本,但是根据作用域的工作原理,尽管循环中的五个函数分别是在各个迭代器中分别定义的,但是它们都会被封闭在一个共享的全局作用域中,实际上只有一个i
,换句话说,i
的值在传入内部函数之前,已经为 6 了,所以结果每次都会输出 6 。
for(var i=1; i <= 5; i++){
setTimeout(function(){
console.log(i);//6
},0)
}
解决上面的问题,在每个循环迭代中都需要一个闭包作用域,下面示例,循环中的每个迭代器都会生成一个新的作用域。
for(var i=1; i <= 5; i++){
(function(j){
setTimeout(function(){
console.log(j);
})
},0)(i)
}
也可以使用let
解决,let
声明,可以用来劫持块作用域,并且在这个块作用域中生明一个变量。
for(let i=1; i <= 5; i++){
setTimeout(function(){
console.log(i);
},0)
}
总结
简单的说:函数 + 自由变量就形成了闭包。其实并不是特别复杂,只是我们需要在引用自由变量的时候小心作用域的变化。
JavaScript基础系列目录地址:
新手写作,如果有错误或者不严谨的地方,请大伙给予指正。如果这片文章对你有所帮助或者有所启发,还请给一个赞,鼓励一下作者,在此谢过。