一说到javascript里的闭包,大部分人都好像有所耳闻,但又说不太清楚到底是个什么东西。那么闭包到底是何方神圣呢?
闭包是javascript中非常常见,但是又非常神秘的一个概念,有多少人像我一样,看了好多关于闭包的技术文章,却始终没能彻底搞懂闭包的深层次原理,经常是面试前突击一下闭包相关的文章,似懂非懂,过段时间后又几乎全忘了,只留下些残破的似是而非的记忆。
今天我尝试着来总结一下我自己对于闭包的理解,加深理解。
先来看一段非常常见的代码:
function getSomeFoods(cb) {
let a = 4;
this.$api('getFoods').get({ // http请求
id: 123456777834
}).then(res => {
if(res) {
cb(res,a)
}
})
}
getSomeFoods(function(res, a) {
console.log(a) // 4
})
这是一个http请求函数,请求成功后执行cb回调函数,并将getSomeFoods函数内的a传递出来,并在定义a的函数之外打印或者调用,这样就产生了闭包。
还有个经典的闭包案列:
function goods() {
let a = 4;
function bad() {
console.log(a)
}
return bad;
}
let badz = goods();
badz(); // 4
goods函数返回一个内部函数,并且在其它地方调用,就形成了闭包。
闭包的定义:一个函数内部的函数,在其词法作用域之外的其它地方被调用,该函数对自己词法作用域的引用称为闭包。
什么是词法作用域?和作用域一个意思,只是比较正式的一种说法。
定义:词法作用域就是定义在词法阶段的作用域,你在写代码时将变量和块作用域写在哪里决定了词法作用域。
回到上面的例子,bad()函数的词法作用域是整个goods()函数内部,所以bad()函数可以访问到a,当我们在外部调用badz()时其实是在执行bad()函数,此时已经是在bad()函数的词法作用域之外调用了,而函数可以正常执行,并且访问到了a,这就是形成了闭包效果。
一般情况下,一个函数执行完了我们都会希望该函数的内部作用域被销毁,释放内存,引擎也确实有一套垃圾回收机制来做这件事,但是如果这个函数我想多次引用,不希望被销毁的时候,闭包就能帮上忙了。
good()函数虽然执行了,但他返回了一个bad()函数,可以重复调用,那么good的作用域就会一直存在,没有被回收。
闭包经典案例
for(var i=0;i<5;i++) {
setTimeout(() => {
console.log(i)
},i*1000)
}
我们期望的结果是打印出0~4五个值,但实际输出的是5个5,为什么呢?
先来理一下,循环的终止条件是i>=5,那么当i=5的时候就会终止,循环的执行是很快的,所以当i已经结束循环了延迟函数都还没有执行,而var定义的变量是一个全局变量,i至始至终都是同一个i,但是值却一直在变化,最后加到5才停下,所以执行延迟函数的时候打印的是同一个i且等于5。
原因找到了,那么要想得到期望的结果就有思路了。因为是同一个i导致的错误,那么我们把每一次的i都单独存一份起来,等到延迟函数执行的时候去拿对应的副本不就可以了么?
这不正式闭包的强项么?利用闭包每次生成一个新的作用域,使得延迟函数的回调可以将新的作用域封闭在每个迭代内部,延迟函数执行的时候就能拿到正确的值。
for(var i=0;i<5;i++) {
(function(j) {
setTimeout(() => {
console.log(j)
},j*1000)
})(i)
}
每次循环都执行以下内部的立即执行函数,并把i当参数传递进去,这样就能正常工作啦。
每次迭代创建一个新的作用域等同于需要一个块作用域。es6的let声明有劫持作用域的功能,这里也可以用到:
for(let i=0;i<5;i++) {
setTimeout(() => {
console.log(i)
},i*1000)
}
let声明有一个特殊功能,循环的时候不止被声明一次,每次迭代都会声明,随后的每次迭代都会用上一个迭代结束时的值来初始化这个变量。
这就是我学习和理解的闭包。