写在前面
闭包(closure)是Javascript语言的一个难点,也是它的特色,很多高级应用都要依靠闭包实现。因此,闭包成为前端面试中经常被问到的一个知识点,比如面试官可能会问:“你了解什么是闭包吗?闭包的作用是什么?请谈谈你对闭包的理解”等等。本文是笔者在学习闭包的过程中总结的笔记,如有理解错误的地方,还请各位大佬多多指正~~~
闭包的理解
1、什么是闭包?
-
概念
百度百科原话是这么说的:闭包就是能够读取其他函数内部变量的函数。例如在
javascript
中,只有函数内部的子函数才能读取局部变量,所以闭包可以理解成定义在一个函数内部的函数。在本质上,闭包就是将函数内部和函数外部连接起来的桥梁。 -
概念的拆分理解
-
闭包是什么?
从概念中的“闭包就是能够读取其他函数内部变量的函数”这句话中抽取主谓宾可知,闭包是函数。
-
闭包在哪里?
从概念中的“闭包可以理解成定义在一个函数内部的函数”这句话可知,闭包是定义在函数内部的。
-
闭包能干啥?
从概念中的“闭包就是能够读取其他函数内部变量的函数”这句话可知,闭包能读取其他函数内部变量。
-
-
在代码中理解
语言总是那么苍白无力,那么就让我们通过代码来感受感受到底什么是闭包?
function addCounter(){ //addCounter()是一个函数,(外层函数) var count = 0 //count是一个被addCounter创建的内部变量(局部变量) function add(){ //add()是定义在函数addCounter()内部的函数(闭包) return count += 1 } return add } var closure = addCounter() //定义一个全局变量closure来接收函数addCounter的返回值 console.log(closure) //closure的值就是add函数 console.log(closure()) //closure()就是执行add函数,使count值增1,即count=1 console.log(closure()) //注意这里的执行结果是2了
执行结果:
ƒ add(){ return count += 1 } 1 2
从上面的代码可知,闭包(
add
函数)的存在,允许定义在addCounter
函数外部的closure
在可以读取到函数内部定义的局部变量count
,从而实现count
的增1运算。 -
闭包的作用
-
能够访问局部变量
概念中有这么一句话 “闭包就是能够读取其他函数内部变量的函数”,这里的其他函数就是指闭包的外层函数,外层函数内部定义的变量就是局部变量。
在上述代码中,
add
函数(闭包)能够访问它的外层函数addCounter
内部定义的局部变量count
。 -
保护这些局部变量
保护这些局部变量是指,闭包可以保护这些局部变量在函数执行完后不被销毁,一直保存在内存中。
在上述代码中,局部变量
count
就是这样受到闭包的保护,在函数addCounter
执行完毕后,没有被销毁,一直保存在内存中,所以后面可以通过closure
修改值。
-
2、闭包解决经典问题
面试时非常大的概率会遇到下面这道面试题,问你输出什么?
for(var i = 1; i <= 5; i++){
setTimeout(function(){
console.log(i)
}, 1000)
}
-
输出结果分析
第一次见到这种题时,很自然的觉得会每隔1s连续打印1 2 3 4 5。
然而事与愿违,这个程序的执行结果却是1s后同时打印出5个6。
要想知道这个代码的输出结果,那么就需要了解这个程序的执行顺序。我们之所以会认为每隔1s连续打印1 2 3 4 5,是因为我们默认每循环一次,
console.log(i)
就会执行一次。事实上,setTimeout
是一个异步函数,它的第一个参数是一个回调函数,这个程序的正确执行顺序是,先执行完循环体,(可以理解为此时已经复制好了5份setTimeout
函数),此时的i
已经是6了;然后在1s后开始回调setTimeout
中的函数function(){console.log(i)}
,因此会在1s后同时打印5个6。 -
代码的修改
当你回答这个程序的执行结果是1s后同时打印5个6后,面试官会问你如何修改代码,可以输出1 2 3 4 5?
修改代码的方式有很多种,这里主要解释以下两种解决方案:
-
方案1:直接将
var
改为let
for(let i = 1; i <= 5; i++){ setTimeout(function(){ console.log(i) }, 1000) }
为什么改为
let
就可以了呢?原因是let
有块级作用域,而var
没有。在这段程序中,如果改为
let
关键字,那么在每次循环时,let
的值就会和循环体绑定在一起,这样在回调时,就不会输出同样的值了。关于
let
和var
的区别,可以看笔者的往期文章 前端学习之面试题系列:(一)var、let、const的区别关于
let
的深入理解,可以看这篇文章 我用了两个月时间才理解let -
方案2:利用闭包
for(var i = 1; i <= 5; i++){ (function(j){ setTimeout(function timer(){ console.log(j) },1000) })(i) }
我们首先利用立即执行函数将
i
的值传给j
,这样每次循环的i
值都会被保存在函数内部,当执行闭包函数timer
时,就可以访问外部函数中保存好的j
值,而不是循环结束后i
的值了。 -
其余方案:
-
利用
setTimeout
函数的第三个参数保存i
值for(let i = 1; i <= 5; i++){ setTimeout(function() { console.log(i) },1000,i) }
-
将
setTimeout
函数的第一个参数改为立即执行函数for(var i = 1; i <= 5; i++){ setTimeout(console.log(i),1000) //第二个延迟时间参数不起作用 }
for(let i = 1; i <= 5; i++){ setTimeout((function() { console.log(i) })(),1000) //第二个延迟时间参数不起作用 }
-
-
-
参考文章
关于JS闭包
闭包
彻底理解setTimeOut
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!