一句话说:闭包就是携带状态的函数
原始闭包
// 原始闭包
function package() {
var a = 0
function lock() {
console.log(a++);
}
return lock
}
/**
* package()() // 0
* package()() // 0
*/
var packIt = package()
packIt() // 0
packIt() // 1
我们通过原始闭包来看四个点,以后的例子都会围绕这四个点诠释“闭包”:
- 条件:作用域的嵌套,在这里我们是
package
嵌套着内部作用域lock
- 条件:内部作用域(锁)
lock
引用了 外部作用域(包)package
的 变量a
并且被调用 即return lock
(我称之为锁住) - 核心特性:经过前两个步骤,闭包已经产生了!由于我们前两步的条件,就像一个包关上了一把内部的锁,导致
package
里面的状态a
不会被销毁,package
函数(包)是具有状态的! - 使用,也是闭包的意义所在但不是必须的,我们用变量
packIt
存着return
出来的lock
,保证lock
不会被使用完后就销毁,这样这个存着状态的作用域(包)也就有了意义,而不是pack()()
这样每次都新建一个包(然后被销毁)
缓缓理解一下闭包的核心(锁包),现实中的闭包不会写的如此“清晰显眼”,我们来通过这4点深入理解不那么显眼的闭包:
实际中的闭包
实际中的闭包未必包含第四步“使用”,有很多就是单纯产生了一个包,我们接下来就是要搞清楚哪里产生了包,不先弄清楚还谈什么用呢?对吧。
// 不显眼的闭包1
function package() {
var a = 0
function lock() {
console.log(a);
}
use(lock)
}
function use(fn) {
fn()
}
package()
这没太大变化,她也是一个闭包,不同于原始闭包,只是内部函数lock
的调用方式变了而已,但方便我们理解一句话:
无论通过何种手段将内部函数传递 到所在的词法作用域以外,它都会持有对原始定义作用域的引用,无论在何处执行这个函数都会使用闭包。
这句话有助于我们理解下面更不显眼的闭包。
// 不显眼的闭包2
function wait(msg) {
setTimeout(function timer() {
console.log(msg);
}, 1000);
}
wait('hello')
这为什么也是一个闭包呢?作用域嵌套,内部引用外部都有了,那内部函数timer
是如何传递 到外部的呢?
工具函数setTimeout(..)
持有一个对参数的引用,引擎会调用这个函数也就是timer
(具体如何实现我们在这里不做深入探讨)
// jQuery中
function showName(name, selector) {
$(selector).click(function show() {
console.log(name);
})
}
showName('box1','#box_1')
showName('box2','#box_2')
回调函数(在另一个地方调用内部函数)也产生了闭包:
在定时器、事件监听器、Ajax请求或者其他(异步同步)任务中,只要使用了 回调函数 ,实际上就是在使用闭包
上面讲了这么多,尽管我们产生了这么多闭包,实际上却没有好好使用闭包的特性,最后我们来讲第四步: 使用
闭包的使用
function myModule() {
function doSomething() {
console.log('doSomething');
}
function doOtherThing() {
console.log('doOtherThing');
}
return {
doSomething:doSomething,
doOtherThing // es6简写
}
}
var m = myModule()
m.doSomething()
m.doOtherThing()
我们将闭包运用到模块当中,m
这个模块是一个具有状态的函数 ,状态指的就是dosomething
等方法,也可以是其他对象、变量 等等都行,他们都作为内部对象 被返回出来,被锁住,外部就可以调用,而不被返回出来的也就是不被调用的变量 则永远无法被外部接触,除非我给你这个接口。
再看一个函数的柯理化:将多参数的函数变成逐层传入的单参数函数。
function makePow(n) {
return function (x) {
Math.pow(x,n) // 计算x的n次方
}
}
var pow2 = makePow(2) // 计算2次方
var pow3 = makePow(3) // 计算3次方
pow2(6) // 36
pow3(3) // 27
这是函数式编程的冰山一角,我们将需要传入两个甚至多个参数的函数进行封装,方便重复使用,而不是每次都传入多个参数。就好比一堆样本中的许多数据都相同,不同的总是那几个,就没必要每次都输入所有数据,就像我们经常使用平方而不是4次方。
闭包带来的问题
最后我们用上面模块化的栗子举一个常见的垃圾回收上的问题:
// 利用閉包封装函数
(function myModule(w) {
{ // 显式声明块作用域
let someReallyBigData = {bigD:xxx}
let btn = document.getElementById('myBtn') // 假设我们有一个按钮
let clickIt = btn.addEventListener('click',fn) // 伪操作
}
function doSomething() {
console.log('doSomthing');
}
function doOtherThing() {
console.log('doOtherThing');
}
w.$m = { // 挂载在window对象上
doSomething,
doOtherThing
}
})(window)
$m.doSomething()
$m.doOtherThing()
当我们的闭包暴露出私有变量 之后,而剩下的变量(代码中的btn
等)也会被锁在闭包的作用域中,js的机制极有可能不会将其清除,而一直残留在内存中,如果数据非常大且杂乱会导致未来无法预料的不好现象,这就是闭包带来的缺点。
因此我们希望将没有暴露的私有变量被销毁,可以显式的(大括号包住的部分)提供一个块作用域,就是let
声明所在的部分,js引擎会知道这部分没必要保留(如果没有暴露接口的情况下)。
上述闭包封装得更规范一点就是这样
(function createModule(w) {
class createObj {
constructor() {
this.something = 'sayIt'
this.otherThing = 'sayOther'
}
doSomething() {
console.log(this.something);
}
doOtherThing() {
console.log(this.otherThing);
}
}
w.$m = new createObj()
})(window)
$m.doSomething() // log 'sayIt'
文章为作者原创,如有不正之处还望大家指出,也欢迎分享讨论