理解闭包是前端开发工程师的基本。 ——鲁迅
1. 闭包是什么
要讲清楚闭包,首先要先从js的作用域说起。
js在运行之前会建立起一条作用域链:
1 | var glob = 'Global'; |
上述代码的作用域链为Global => Outer => Inner
,作用域链越靠近末端,可以访问的变量范围越大。任何一个函数可以访问其自己所在作用域中的变量以及父作用域中的变量,比如在Inner
函数内部可以访问到glob
、outer
、inner
三个变量。
函数无法访问自己子作用域中的任何变量,除非定义为全局的(不推荐):
1 | var glob = 'Global'; |
js中函数是一等公民,也就是说函数可以作为任何值被传递,所以看上边的例子,虽然在全局环境无法访问outer
变量,但由于Inner
函数可以访问,如果在全局环境中获取Inner
函数的话,就可以访问outer
变量了:
1 | var glob = 'Global'; |
上述的func
接受了Outer
函数返回的Inner
函数,所以func
即可访问Outer
函数中定义的outer
变量了。举个简单的例子:
1 | function Outer() { |
这就是外部读取内部变量的经典方式。这个Inner
的内部函数,就叫做闭包。闭包
简单来说就是函数内部的函数,如果一旦该内部函数被返回到别的作用域,那么这个内部函数就形成一个闭包。因为引用了函数内部的一些变量,所以可以在别的作用域中进行访问。
同一个闭包可以同时存在多个,例如:
1 | function Outer() { |
所以,在本质上,闭包就是将函数内部和函数外部连接起来的一座桥梁。
2. 闭包的副作用
首先由于闭包会携带包含它的作用域,所以闭包会比其他函数占用更多的内存。过度使用可能会导致内存泄漏。
另一方面,闭包还有一个副作用,就是闭包只能取得包含函数中任何变量的最后一个值。
1 | function createFunctions() { |
你会发现,result
这个数组其实是一串闭包的数组,因为都引用了i
这个变量。
表面上看,数组里的每一项运行后都能返回它们各自的索引值,从0-9,但其实,每次运行的结果都是10。因为这10个函数中都引用了createFunctions
里边的i
变量,这个变量在函数执行完成的时候为10,所以所有的结果都是10。如果想要改成预期的结果,则可以这样做来避免上述情况:
1 | function createFunctions() { |
另外,关于this
对象,闭包有时候也会有一些需要注意的地方,举个例子:
1 | var name = 'The window'; |
上述三个例子,object1
是正常情况,object2
调用getName()()
后发现this
的指向是全局的,因为object2.getName()
执行后返回的是匿名函数function(){return this.name}
,因为匿名函数的执行环境具有全局性,所以这个this
指向全局环境中的name
。如果想要返回的函数返回对象里边的name
,那么就要像object3
一样,构建一个闭包,引用内部的this
,将值放到that
中,然后返回调用即可。
3. 闭包的例子
1 实现私有变量
1 | var app = (function() { |
2 knockout中的observable
1 | var far = ko.observable('LinWei'); |
3 未完待续…
4. 参考文献
- 学习Javascript闭包(Closure) 阮一峰
- Javascript高级程序设计(第五版) 第七章-函数表达式