闭包在JavaScript中无处不在,但是对于很多人(我)来说,他们(我)还是很难去理解这些闭包现象,希望通过这篇文章可以帮助大家(自己)理解闭包。
理解闭包
在《你所不知道的JavaScript上》中很好的解释了闭包的定义,😎 当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行。
然后我们马上来举一个🌰:
function foo() {
var a = 2;
function bar() {
console.log( a );
}
return bar;
}
var baz = foo(); // 相当于bar函数在foo函数外部执行(bar函数所在的词法作用域外)
baz(); // 还是引用了bar所在词法作用域内的变量,输出2
简单概述一下上面的代码执行过程:
-
声明了一个foo函数,声明一个变量baz,赋值为foo()的返回值
-
执行foo(), 声明函数bar,变量a,然后返回bar
-
baz得到返回值,就是bar函数,执行baz()函数
-
执行console.log(a), 向上一层作用域内寻找变量a,在foo函数内,bar函数记住了其所在的词法作用域,并给return了出去。在进行baz=foo()之后,正常来说我们希望foo()执行后,其内部的作用域被销毁(javascript引擎的垃圾回收机制, 会释放不再使用的内存空间),因为在执行baz=foo()之后,看上去我们不再需要使用foo内部的内容了,但是事实上foo函数的内部作用域依然存在着,那么是谁在一直使用该作用域导致了其没有被垃圾回收呢,是bar函数,bar函数拥有着涵盖foo()的内部作用域的闭包,使该作用域一直存活着,console.log(a)中查询变量a即为foo的内部作用域中的变量a, 输出为2。
好的我讲完了,但似懂非懂,找些例子看看。
例子1 最常见的axios接口封装
import request from "@/api"
const exportRecordList = (data) => {
return request({
url: `/api/xxxxxxxxx/export`,
method: 'post',
data
})
}
exportRecordList({age:18})
😶 request函数有着对其所在作用域的闭包(request函数中引用了所在作用域中的data变量),执行完exportRecordList()后,因为返回的函数中有着对exportRecordList()内部作用域的引用,其内部作用域不会被销毁,保持完整。
例子2 一个延时输出的函数
const wait = (str) => {
setTimeout(function logStr() {
console.log(str)
}, 1000);
}
wait('hello, world')
🥲 logStr函数内有着涵盖wait()函数作用域的闭包, 和上面类似。
例子3 一个经典的面试题
for (var i=1; i<=5; i++) {
setTimeout(function timer() {
console.log( i );
}, i*1000);
}
😥 i=1的时候,var声明的变量挂载到全局上,window.i = 1,执行setTimeout,1s后打印i 😥 i=2的时候,var声明的变量挂载到全局上,window.i = 2,执行setTimeout,2s后打印i
...
😥 i=5的时候,var声明的变量挂载到全局上,window.i = 5,执行setTimeout,5s后打印i 😥 i=6的时候,var声明的变量挂载到全局上,window.i = 6, 不满足i<=5跳出循环
上面的循环大概在几微秒内就完成了,然后过了1s开始打印console.log(i),引擎向作用域讯问 i变量是 啥,作用域向上寻找找到了 i = 6,打印出6, 过了2s继续打印console.log(i), 还是6...
到最后输出结果为 6 6 6 6 6
用闭包改造一下
for (var i = 1; i <= 5; i++) {
(function fn(j) {
setTimeout(function timer() {
console.log(j);
}, j * 1000);
})(i);
}
- i=1的时候,fn接收到i的传参,声明变量j = i (1), timer函数内有着对其所在作用域的引用console.log(j),此时fn()的作用域保留, j = 1, 1s后输出1
- i=2的时候,fn接收到i的传参,声明变量j = i (2), timer函数内有着对其所在作用域的引用console.log(j),此时fn()的作用域保留, j = 2, 2s后输出2
...
- i=5的时候,fn接收到i的传参,声明变量j = i (5), timer函数内有着对其所在作用域的引用console.log(j),此时fn()的作用域保留, j = 5, 5s后输出
- i=6的时候,不满足条件,跳出循环,此时window.i = 6。
- 最后输出
1 2 3 4 5
当然啦,也可以直接用let声明
let
允许你声明一个作用域被限制在块
作用域中的变量、语句或者表达式
for (let i = 1; i <= 5; i++) {
setTimeout(function timer() {
console.log(i);
}, i * 1000);
}
输出 1 2 3 4 5
例子4 模块化
function CoolModule() {
var something = "cool";
var another = [1, 2, 3];
function doSomething() {
console.log( something );
}
function doAnother() {
console.log( another.join( " ! " ) );
}
return {
doSomething: doSomething,
doAnother: doAnother
};
}
😭 如果我们调用了CoolModule(),doSomething()和doAnother()都将拥有涵盖CoolModule()内部作用域的闭包
小结
当函数可以记住并访问所在的词法作用域,即使函数是在当前词法作用域之外执行,这时 就产生了闭包。