前言
“闭包”,可以排进JavaScript最难理解概念的前三,连那些正在从事“前端”职业的人,可能都没懂。
这么说不是吓唬人,它并不难,但它的名字本身就有点不友好,“闭”什么?“包”什么?“闭什么包”?(此处自行脑补马某梅~)
技术圈儿里从不缺少这类一部分人很懂,一部分人很不懂的晦涩概念。
所以,在此先把名字的意思重塑一下,闭包 == 封闭 + 包裹。
作用域
“闭包”就是一种突破数据访问能力的方法。
那么本身的访问机制是怎样的呢?
我们知道,JavaScript的作用域,分全局和局部,局部其实也可称为“块级”。
“块”即一对大括号包含的代码块。
1 | { |
不论是循环,还是条件判断语句,都在此范畴,但是在ES6之前,这样的包裹没有起到限制作用域的效果,所以,JavaSctipt的局部作用域通常由“函数”构成。
变量定义在全局,就全局可访问,甚至修改,但定义在局部,只在局部可用,外部不可访问。
函数就是闭包?
相信很多人曾为搞懂“闭包”看过不少资料,看着看着,就会看到这句话“函数就是闭包”。
这句话简直是雪上加霜,为理解“闭包”增添了新一层迷雾。
别怕,我们来段代码:
1 | function outer() { |
得嘞,这就是闭包达成的效果~
我特意使用了方便理解的命名方式,这段代码的特点是:
- 全局作用域定义了一个函数 outer
- outer 内部嵌套定义函数 inner
- inner 访问了 outer 的变量a
- outer 执行结果返回 inner
- 返回的结果赋给了另一个全局变量 global
于是,效果就是,global 成功访问了 outer 的所管辖区域的变量。
这一路看下来是不是觉得挺正常的?可是,它本来无法访问的呀~
先来看一句话:
JavaScript的函数可以嵌套在其他函数中定义,这样它们就可以访问它们被定义时所处的作用域中的任何变量。这意味着JavaScript函数构成了一个闭包(closure),它给JavaScript带来了非常强劲的编程能力。
这句话来自《JavaScript权威指南》函数章节的引言,有两个重点:
一、函数构成闭包
二、可访问它被定义时所处的作用域
所以,函数并不是闭包,而是闭包形成的土壤,上例中的global只是作为一种引用标识符,调用的还是内部的inner。
可访问的,不是函数被调用时的作用域,而是定义时,这么一说,似乎一切都合理了。
或许你有一种常识——垃圾回收机制,不再使用的内存空间会被释放掉。当函数在被调用、代码执行之后,其内部作用域就被销毁了,闭包的神奇之处就是消除这种机制所造成的影响。
到这儿,我们回头再看看“封闭+包裹”是什么意思,“闭”就是局部作用域,而“包”是作用域的嵌套。
还有么?
它只有一种形式吗?答案是否定的,不然它就没那么多用处了。
只要是传递了一个函数类型的值,不论形式,当函数在别处调用,都可以看到闭包的形成。
比如这样:
1 | function outer() { |
这段代码中,anothier 是 outer之外的一个函数,但因为它传递了一个outer内部的函数 innner,继而能够访问到 outer 中定义的a。
再看:
1 | var fn; |
这种像是前面两者的结合体,先定义了全局变量fn,在函数内部将inner赋给了fn,然后在another里调用fn()。
以上几段代码,形式不同,但原理一致。
它在哪儿?
到这儿大家可能已经懂了什么是闭包,但上面的讲述只是为了方便理解,实际项目中,它不会那么乖巧、坦诚的暴露给你,它可能隐藏在成百上千行的代码中,所以到底哪些地方用了闭包?或者说它有什么用?
示例一
1 | function wait(msg){ |
我们写了个wait函数,其中设置了定时器,定时器内有个timer函数,然后调用wait函数,并传参。
wait函数是马上执行的,但timer在1000毫秒之后才执行,这时候wait的作用域应该是已经被销毁了,但是依然可以正常输出”你好, 闭包!”。
示例二
1 | function clickBtn(name, selector) { |
这段代码可看出,我们给某按钮设置了负责点击行为的函数,在函数内部绑定了事件监听器,这样以来,也出现了作用域的嵌套,形成了闭包。
以上两个示例均使用了“回调”,这是一种很常见的闭包形成方式,定时器、事件监听器、Ajax请求等都存在这种情况。
“坑”
这个坑大概所有人都掉过,就是循环。
1 | for (var i=1; i<=5; i++) { |
这段代码目的很明显,从1开始,每隔一秒输出一个数字,每次递增1,正确的结果是1,2,3,4,5。
试着运行它就会发现“诡异”的现象,它居然输出了5次6?5次好理解,6是什么?!
这就要追究到js的任务运行机制,在循环体中,遇到定时器、事件监听器或其他类似执行体的时候,会先执行循环,将i从头到尾存储到一个栈当中,而里面的函数并不会在i值变化的时候马上执行,会依次进入任务队列,for循环结束后,队列中的函数才进入主线程执行,所以,这里i执行完最后一次迭代就是6,且被5个定时器输出了5次。
怎么解决?较为常用的一种办法是:
1 | for (var i=1; i<=5; i++) { |
我们加了个立即执行的匿名函数,这就为每次迭代创建了一个闭包环境,即使里面的函数体依然会等待执行,但正确的值已经被每个独立的作用域保存起来,执行的时候就能输出预期的结果。
慢着,这么做不就是为了得到一个封闭的作用域吗?既然如此,干脆这样咯:
1 | for (let i=1; i<=5; i++) { |
ES6的出现给了我们更干净的处理方式,终于不用动歪脑筋了~
更大的用处
闭包的一项本领是什么?——局部作用域中的变量,跟外界相互独立,但可通过调用内部定义的函数访问。
而这正是模块化所需要具备的:
- 隐藏私有数据
- 暴露共有方法
所以,我们可以这么写。
1 | function Module() { |
是不是很熟悉?也感受到了闭包的无处不在和强大?
当然,模块化有多种形式,本文点到为止,后面单独介绍。
小结
对于“闭包”,我们需要了解:
- 它是什么
- 表现形式
- 应用场景
懂不懂“闭包”似乎成为工程师们的一道坎,相信读罢此文,你已经豁然开朗,如有问题,欢迎交流~
下篇见!