Javascript闭包(closure)众说纷纭,对入门者来说是个可能有点迷的概念和技术。这里给出一种解读,希望绑到有需要的人。
省流不看,一句话解说
闭包就是当你想调用一个函数A,你无法马上调用函数A,而要先调用另一个函数B,函数B的返回值才是函数A。闭包就是关于为什么要这样做以及怎么做到的问题。
大多数讲解闭包的文章,都是侧重于讲闭包在技术层面的实现,而忽略了闭包解决什么问题。如果你不知道一项技术是解决什么问题的,那么就很难真正掌握这个技术。所以接下来先用一点篇幅说一下闭包解决的是什么问题。
问题是什么?
函数式编程语言,核心就是函数和变量。当你调用函数,你把若干变量输入到函数中,然后函数给出一个结果(也是一个变量)。例如最简单的,输入a和b,得到c是a+b的结果。
var a = 10000 // 账户初始余额
var f = function(x){ a += x; } // 改变工资账户余额的函数
var b = 5000 // 本月新发工资
f(b) // 最新余额15000
同一个函数可以反复调用:
var a = 10000 // 账户初始余额
var f = function(x){ a += x; } // 改变工资账户余额的函数
f(5000) // 发薪5000元,最新余额为15000
f(5200) // 发薪5200元(涨薪200块),最新余额为20200
全局变量a是很混乱很危险的,可以被任意函数改动。
第一层问题:有些变量,不应该允许任意函数修改它,而希望是只允许我们特别限定的一个或几个函数去改变它。
第一层问题是变量(或者说数据)的空间问题,它应该在空间维度上被保护起来。
解决第一层问题最简单的粗暴的办法当然是把全局变量改为函数内的局部变量,但局部变量又会引发另一个问题——局部变量在函数进入时新开辟空间、函数退出时释放。对于改变账户余额的函数来说,如果每次执行都会重置余额,这就不仅仅是尴尬,而是致命的。由此引出第二层问题:
第二层问题:有一些空间上受保护的变量,还希望在时间维度上被保护起来,在函数多次调用中,这个变量的值能保持和传承。
第二层问题是变量的时间问题,它需要保持。
综上,问题就是要对某个或某些变量实现“空间保护+时间保持”。
有什么好的想法?
函数的局部变量本身就是受保护的,而且这也是唯一一种保护变量的办法。
因此,必须用函数局部变量来解决问题,但有一点变通。
局部变量的开辟和销毁,一定是发生在函数的进入和退出两个阶段,在中间阶段,它既是被保护的,也是可靠“保持”的。那么,我们不要只盯着修改账户余额的函数,而是额外定义一个函数,让它负责保护并保持账户余额变量,并且让它在我们整个的程序运行一开始就进入、直至整个程序退出时它才退出。
// 额外的函数,并不是用来修改账户余额的,而是用来保护+保持账户余额的
var g = function() {
var a = 10000 // 这个局部变量被g()函数“保护”
// ....
// ....
// ....
// ....
// 到这里,在g()函数彻底退出之前,a一直都是“保持”的
}
现在考虑真正用来修改账户余额的函数f()面临的挑战:
第一,f()函数必须能访问到受保护的变量a;
第二,f()函数所有的调用,都必须发生在g()函数彻底退出之前。换句话说,只要f()还可能被调用,就必须阻止g()退出;
思路其实呼之欲出了。
闭包的实现
闭包的实现,至少有2个函数。一个是负责“包”,是辅助性的;另一个“被包”,被包的就是真正想要的函数。
首先是“闭包函数”,它的局部变量会被保护:
var g = function() {
var a = 0; // 账户初始余额
}
其次是“被包函数”,为了访问a变量,它必须定义在g()内部:
var g = function() {
var a = 0
var f = function(x) { a += x } // 真正用来改变余额的函数
}
再次,对f()的调用,必须在g()以外,否则如果在g()内部调用f(),就失去了对a的保护。
在里面定义、在外面调用,那么g()把f()函数当做返回值送出去就可以了:
var g = function() {
var a = 0
var f = function(x) { a += x }
return f // 把f当做返回值
}
var f = g() // 调用g,得到返回值并赋值给全局变量f,也就是一个来自g()内部的、真正用来改变余额的函数
特别注意:g()函数把自己的局部变量f返回出去了,并且被全局的f变量所引用。一个函数如果其局部变量被外部任何有效的变量引用,该函数就不会销毁相关资源。
至此,闭包就初步完成,全局的f就具备了闭包性质(初步完成的完整范例):
var g = function() {
var a = 0
var f = function(x) {
a += x
return a // 为了便于验证,每次改变余额,都把最新余额返回
}
return f
}
var f = g() // 这个f函数具备闭包性质
console.log(f(5000)) // 发薪5000,余额5000
console.log(f(5200)) // 发薪5200,余额10200
console.log(f(-9600)) // 消费9600,余额600
把代码整理一下,精简一下不必要的变量声明:
/**
var g = function() {
var a = 0
var f = function(x) {
a += x
return a
}
return f
}
**/
// 原本的var f = g(),其中的g用上面的var g = function(){}“等价代换”了(套上了括号)
var f = (function() {
var a = 0
/**
var f = function(x) {
a += x
return a
}
**/
// 原本的return f,其中的f用上面的var f = function(){}“等价代换”了
return function(x) {
a += x
return a
}
})()
console.log(f(5000))
console.log(f(5200))
console.log(f(-9600))
总结一下
用闭包实现了我们这个账户管理函数之后,费如此力气,有什么收获和好处?
首先,避免了混乱而危险的全局变量。闭包里的那个a,现在有非常高的访问安全性,在f()函数定义以外的地方,你没有办法随意改变它;
其次,你真正使用的函数f,它现在非常专注于其职责,具有更明确的现实语义(收入或支出),它面向的是语义,而不是程序实现。
基于以上两点好处,其实“面向对象”的概念已经呼之欲出,了解面向对象的初学者可能会说这些用对象很容易实现。闭包其实可以认为是给了函数式编程语言一定的面向对象特性,或者说,面向对象就是基于函数式语言闭包的技术和思想而发展出来的。
另外,闭包是大多数函数式编程语言都具备的东西,而不是Javascript特有。
更进一步
来一个稍微现实一点、复杂一点的范例代码吧:
// 这个名为Account的函数,用来表示一个银行账户
var Account = function(name, id) {
var name = name // 闭包变量,账户名
var id = id // 闭包变量,账户号
var remaining = 0 // 闭包变量,账户余额
// 从闭包内返回的不是单一函数,而是多个函数组成的简单对象
return {
// 账户支出
pay: function(x) { remaining -= x },
// 账户收入
income: function(x) { remaining += x },
// 显示账户余额
about: function() { console.log(name, '的账户#', id, '余额为:', remaining) }
}
}
var a1 = Account('张三', '6655835588869353')
var a2 = Account('李四', '6655835588869300')
// 初始余额显示
a1.about()
a2.about()
a1.income(5000)
a2.income(5200)
a1.about()
a2.about()
a1.income(5000)
a2.income(-2400)
a1.about()
a2.about()