闭包闭包,在我看来,这名字除了听起来抽象,高大上一点,没别的优点。
一、跟着我来搞懂“闭包”是啥
没了解过的人看起来:跟“封装”一样
没了解过的正常人看到这名呢,闭包嘛,“闭合成一个包咯”,那就是对象将自己包装起来嘛。
乍一看,这不是封装吗?咋还起个“闭包”的小名。
你说闭包和“封装”能扯上太多关系吗,其实也有一丢丢(下面我会解释一下下),但能一样吗,肯定不能呀。所以这名就起得就不好,不清晰还容易误导人,不能做到见名知意。
封装,即隐藏对象的属性和实现细节,仅对外公开接口。
了解过的人看起来:这和“刺猬”包起来保护自己一样
那稍微了解过闭包的人又说了,“闭包”这名也有意思,像“刺猬”一样将自己包起来保护自己,防止自己被吃掉(垃圾回收机制回收)。
说到这里,其实我已经部分赞同这种说法了。
很多人是说不清闭包是什么性质的东西,抽象的?实体的?或者是行为?还是什么奇奇怪怪的东西?
在我看来,这部分人能说出“包起来,防止被垃圾回收机制回收”证明他们已经有过些许思考了。当然我们也可以很容易看出来,在他们看来,“闭包”其实是一种行为。
但是,对吗?闭包是一种行为吗?如果对,所谓的包起来是怎么包?又是怎么防止被回收的?
我认为不对。为啥?往下看。
如果不是“保留自身”的行为,那闭包是啥?
闭包的存在的确有“保留自身”的作用。但这只是附带功能,并非主要功能,所谓“刺猬”的想法是也是有些许“年轻”的。
众所周知,用完之后不会再没用到的东西,就会很容易被丢掉。
JS垃圾回收机制就充当着回收的角色,它认为该数据没有用了,就会丢掉,其实也是一种优化内存的存在。像下面一个例子:
function foo() {var a = '我是a啦'console.log(a)}foo()
foo函数调用完后,正常来说它调用时创建出的foo函数作用域已经没有别的作用的,这种“没用的东西”应该会被回收掉,是的,所以这里的话,创建出的foo函数作用域用完就会被回收掉。
但是下面这样呢?
function foo() {var a = '我还是a啦'function print() {console.log('a说:' + a)}return print}var bar = foo()bar() // a说:我还是a啦bar() // a说:我还是a啦... // 此处省略无限次bar调用, 都是输出:a说: 我还是a啦
一段有趣的对话:
foo函数作用域:“啊哈,我可不是没有用的东西!”
“怎么说?”:我
foo函数作用域:“没看到吗?我和我家小a还在。垃圾回收没把我们扔了。”
“是哦,你怎么做到的?”:我
foo函数作用域:“这还不简单!”
foo函数作用域:“我跟垃圾回收说,我给别人承诺了,我给别人一张借条,承诺如果她需要我的东西,就通过这个借条给我拿。”
“它(垃圾回收)就这样就放过你了吗?”:我
foo函数作用域:“那是,它不放过我,以后谁履行承诺!它那个怂货可背不起责任!”
有的人看到这,就会说:“噢!我知道了!它保留了自己,这就是闭包!”
瞧吧,我要是老师非把这类人叫出去罚站。前面的文章不好好看,都说这才不是闭包!这是闭包的附带功能“保留自身”,不过准确来说应该说这是“闭包的效果”!
不过真正闭包的确隐藏在其中。
那真正的闭包是什么?“借条”!
“我跟垃圾回收说,我给别人承诺了,我给别人一张借条,承诺如果她需要我的东西,就通过这个借条给我拿。”
——上面的foo函数作用域
看到了吗?这个“借条”才是闭包!
因为这个借出承诺,垃圾回收机制不会把foo函数作用域当成不再使用的东西,因为随时有可能有人通过这个承诺来给foo作用域借东西,垃圾回收机制要是把它回收往哪借?要是借不到在程序中可是寻值失败甚至可能报错,所以它才不会引火上身。
那这个借条要是用js的角度去看到是啥呢?
bar 持有 foo函数作用域 给她的 借条。
bar 持有对 foo函数作用域 的 引用。
我觉得很清晰了,上面可以看出以下等式:(这里不完全正确,请继续往下看)
闭包 = 借条 = 函数作用域引用
所以呢。闭包是行为吗?非也。它是抽象的吗?非也。那是实体吗?摁…你说引用是不是实体,如果你不知道引用是什么,可以不用那么快了解闭包的[手动狗头]。所以闭包其实是实体,并非行为,也并非抽象。
二、闭包诞生的整个过程的解析如下:
第一步、首先得有个封装的函数
为什么?
摁…不封装,大家都在同一个全局作用域下,还需要“借条”吗?想用就拿去用了。(所以你要说闭包和封装没有一点关系,还真不是,封装甚至可以说是前提,要借出“借条”,得先有借出“借条”的人吧!)
然后我们折腾折腾出了foo函数。
function foo() {var a = '我还是a啦'function print() {console.log('a说:' + a)}// 看到了吗?这个返回是关键噢,它通过这个向外抛出“借条”return print}
到这一步,其实都还没闭包的一点影子,甚至它的爹爹foo函数作用域都还没被造出来。
作用域的知识点就不讲了,只有函数被调用了,才会创建它对应的词法作用域。
第二步、调用foo函数
foo()
说到调用,有些人就写了上面那句,然后就说:“好了,这样闭包算是创建出来了吧”。
错,大错特错。
foo抛出的借条给谁了?没有!垃圾回收机制不是傻的,它知道没人给你借东西,照样把你给回收了!所以说没有人认领的借条就不是合格的借条,也自然不被当成闭包**。**
第三步、必须保证被持有
var bar = foo()
到了这一步,闭包才算是真正被创建,所以闭包创建必须满足两个条件:
① 由特定作用域向外抛出对内数据的引用。
② 有其他作用域内的变量持有该引用。
第四步、借来为所欲为
bar()... // 省略无限次bar调用
当“借条”被持有时,foo作用域不用担心被回收掉,持有“借条”的作用域也可以将借来的数据为所欲为,比如:调用无限次。
第五步、假如丢失了“借条”呢
// 多手写了一句bar = function sayGoodBye() {console.log('再见小a,我不爱你了!')}
那恭喜了,foo作用域“挂掉”了,所谓的闭包也不复存在了。
总结
结合上面的讲解,真正的闭包等式应该为:
三、谈谈闭包的使用
解决异步循环输出同个值的问题
for (var i = 0; i < 5; i++) {setTimeout(function() {console.log(i) // 输出5个5 不符合实际需求}, 1000 * i)}
常见吧,刚学js的时候常常遇到的问题。为啥呢
发现如果要讲,真的好多要讲,这里不细讲,但是你要知道几个概念:
① 异步操作会等待同步操作执行完后才执行
② setTimeout是异步操作
③ js中的for语句并不会创建块状作用域,内部变量在同个作用域中共享。
在上面的例子中,,i这个变量会共享于同一个作用域(这里是全局作用域)。
上面的循环i的自增至5都是同步操作,而setTimeout作为异步函数,会等待同步操作处理完后依次处理,
等同步操作完后i等于5,这时setTimeout的异步操作开始执行,自然之后取到的i值都是同一个值5。其实这里的锅就出在Js的for循环是不会创建块级作用域的。所以后来ES6用let就不会有这个问题。
如何解决上面的问题?
// 使用IIFE - 即时调用函数for (var i = 0; i < 5; i++) {(function (temp) {setTimeout(function timer() {console.log(temp) // 输出 0 1 2 3 4 符合实际要求}, 1000 * i)})(i)}
这就是常见的IIFE解决方法。很多人都讲不清为啥这里使用了闭包,最多像最上面的一样说这IIFE保留了各自的作用域,保护了内部的i值,但其实说了也是让人一知半解,现在来看看我的解析。
来看下面这种表示(只是用来讲解的)
global {setTimeout: function (cb, duration) {等待duration秒 {cb()// 等待duration秒执行cb函数}}for (var i = 0; i < 5; i++) {// 使用IIFE - 即时调用函数(function (temp) {function timer() {console.log(temp) // 输出 0 1 2 3 4 符合实际要求}// 这里相当于 将timer函数return出去给外部作用域的setTimeout当参数setTimeout(timer, 1000 * i)})(i)}}
从上面看,我们可以很清晰地看到,IIFE函数作用域中会将外部传进来来的i值保存至自身的temp值中,同时它内部有个timer函数用于输出temp值。
它通过将timer函数当做参数的形式传给外部作用域中的setTimeout函数,而这里就是创建闭包的关键!
它满足了以下两点:
① 向外抛出对自身作用域的引用(将timer函数当做参数的形式传出)
② 外部作用域持有对自身作用域的引用(setTimeout函数中的cb变量实际就持有timer函数的引用)
所以看到了吧,闭包!这就是闭包!还记得闭包有“保留自身”(防止被回收)的能力吧,对的,也就是这个能力再配合每次执行copyi副本能让其暂存在自己作用域的temp变量中。
你可知道回调函数可都是闭包的使用呀!
看下面的例子:
function fuc () {// fuc调用了log函数,并把一个具名函数oper当成参数传给log函数log (function oper() {console.log('正在操作')})}function log (oper) {console.log ('操作前准备')oper()}fuc()
上面的例子其实和上面setTimeout的例子非常像,继续转换为容易解析的形式
来看下面这种表示
function fuc () {var oper = function () {console.log('正在操作')}log (oper)}function log (oper) {console.log ('操作前准备')// 竟然可以在log函数作用域中执行oper()}fuc()
fuc中的oper函数出于fuc函数作用域中,是共享该作用域的数据的。
oper函数被当做参数传给log函数后,log函数可以在自己的函数作用域中调用oper函数,完全是因为它持有oper的引用,也即持有对fuc作用域的引用,以至于fuc函数作用域不会在调用后被回收掉。
这里另外提一句,js是词法作用域,虽在fuc函数中调用log函数,但是log函数作用域并非包含在fuc函数作用域中,这里就不讲太多了。
!所以不要再说闭包用得少了,简直是无处不在。
最后一个思考:你觉得“闭包”要改个名应该叫什么呢?
“借出的借条?正确,但是太长太臭了吧!”:作者
End
参考
- 你不知道的JS(上),强烈建议买这套本书去看。
