从技术角度讲,所有的JavaScript函数都是闭包: 它们都是对象,它们都关联到作用域链。
实现计数器
介绍利用闭包实现计数器,请看以下代码:
var counter = (function() {
var count = 0
return function() {
return count ++
}
})()
以上代码定义了一个立即执行的函数,count遍历只能在这个匿名函数中可以使用。
匿名函数:就是没有函数名的函数。函数的定义,大致可分为三种方式:
第一种:这也是最常规的一种
function double(x){
return 2 * x
}
第二种:这种方法使用了Function构造函数,把参数列表和函数体都作为字符串,很不方便,不建议使用。
var double = new Function('x', 'return 2 * x')
第三种:
var double = function(x) { return 2* x}
注意“=”右边的函数就是一个匿名函数,创造完毕函数后,又将该函数赋给了变量square。
匿名函数的创建
第一种方式:就是上面所讲的定义square函数,这也是最常用的方式之一。
第二种方式:
(function(x, y){
alert(x + y)
})(2, 3)
这里创建了一个匿名函数(在第一个括号内),第二个括号用于调用该匿名函数,并传入参数。括号是表达式,是表达式就有返回值,所以可以在后面加一对括号让它们执行.
自执行的匿名函数
1. 什么是自执行的匿名函数?
它是指形如这样的函数: (function {// code})()
2. 疑问
为什么(function {// code})()可以被执行, 而function {// code}()却会报错?
3. 分析
(1). 首先, 要清楚两者的区别:
(function {// code})是表达式, function {// code}是函数声明.
(2). 其次, js"预编译"的特点:
js在"预编译"阶段, 会解释函数声明, 但却会忽略表式.
(3). 当js执行到function() {//code}()时, 由于function() {//code}在"预编译"阶段已经被解释过, js会跳过function(){//code}, 试图去执行(), 故会报错
当js执行到(function {// code})()时, 由于(function {// code})是表达式, js会去对它求解得到返回值, 由于返回值是一 个函数, 故而遇到()时, 便会被执行.
另外, 函数转换为表达式的方法并不一定要靠分组操作符(),我们还可以用void操作符,~操作符,!操作符……
如:
!function(){
alert("另类的匿名函数自执行")
}()
匿名函数与闭包
闭包的英文单词是closure,这是JavaScript中非常重要的一部分知识,因为使用闭包可以大大减少我们的代码量,使我们的代码看上去更加清晰等等,总之功能十分强大。
闭包的含义:闭包说白了就是函数的嵌套,内层的函数可以使用外层函数的所有变量,即使外层函数已经执行完毕(这点涉及JavaScript作用域链)。
function checkClosure(){
var str = 'rain-man'
setTimeout(
function(){ alert(str)} //这是一个匿名函数
, 2000)
}
checkClosure()
这个例子看上去十分的简单,仔细分析下它的执行过程还是有许多知识点的:checkClosure函数的执行是瞬间的(也许用时只是0.00001毫秒),在checkClosure的函数体内创建了一个变量str,在checkClosure执行完毕之后str并没有被释放,这是因为setTimeout内的匿名函数存在这对str的引用。待到2秒后函数体内的匿名函数被执行完毕,str才被释放。
用闭包来优化代码:
function forTimeout(x, y){
alert(x + y)
}
function delay(x , y , time){
setTimeout('forTimeout(' + x + ',' + y + ')' , time)
}
/**
* 上面的delay函数十分难以阅读,也不容易编写,但如果使用闭包就可以让代码更加清晰
* function delay(x , y , time){
* setTimeout(
* function(){
* forTimeout(x , y)
* }
* , time)
* }
*/
匿名函数最大的用途是创建闭包(这是JavaScript语言的特性之一),并且还可以构建命名空间,以减少全局变量的使用。
var oEvent = {}
(function(){
var addEvent = function(){ /*代码的实现省略了*/ }
function removeEvent(){}
oEvent.addEvent = addEvent
oEvent.removeEvent = removeEvent
})()
在这段代码中函数addEvent和removeEvent都是局部变量,但我们可以通过全局变量oEvent使用它,这就大大减少了全局变量的使用,增强了网页的安全性。
我们要想使用此段代码:
oEvent.addEvent(document.getElementById('box') , 'click' , function(){})
var rainman = (function(x , y){
return x + y
})(2 , 3)
/**
* 也可以写成下面的形式,因为第一个括号只是帮助我们阅读,但是不推荐使用下面这种书写格式。
* var rainman = function(x , y){
* return x + y
* }(2 , 3)
在这里我们创建了一个变量rainman,并通过直接调用匿名函数初始化为5,这种小技巧有时十分实用。
var outer = null
(function(){
var one = 1
function inner (){
one += 1
alert(one)
}
outer = inner
})()
outer()//2
outer()//3
outer()//4
这段代码中的变量one是一个局部变量(因为它被定义在一个函数之内),因此外部是不可以访问的。但是这里我们创建了inner函数,inner函数是可以访问变量one的;又将全局变量outer引用了inner,所以三次调用outer会弹出递增的结果。
闭包的英文对应的是Closure,如果要单纯的讨论这个概念的话就要提到和图灵机起名的大名鼎鼎的lambda演算(lamdba calculus)。尽管lamdba的概念并不是本文的重点,但是闭包概念的目的便是支持lamdba的实现。如果你单独地在百度对进行搜索闭包的话,你会发现大部分都是js相关的内容,主要是js本身就只用闭包的这个概念。但是闭包并不仅限于js,而是一个通用的概念。借用wiki中有点抽象的定义来说的话,闭包就是:
在计算机科学中,闭包(英语:Closure),又称词法闭包(Lexical Closure)或函数闭包(function closures),是引用了自由变量的函数。
简单来说就是当一个方法引用了方法局部变量外的变量时,它就是一个闭包。而如果根据这个定义继续延展的话,就可以得到另外的一种描述方法:
闭包是由函数和与其相关的引用环境(方法外变量)组合而成的实体。
尽管给出了两种关于闭包是什么的定义,但是闭包本身的概念还是较为的抽象。比如直接给例子,我们可以先讨论一下为什么需要闭包。
为了理解闭包,我们可以先假设这样的一种场景:
我有一个方法A,其中有私有变量。我想在别的方法中可以直接访问并使用它,但是又不想直接暴露它。
这时候,就可以在A建立一个内部类B来访问私有变量。那么这时候,这个内部类中你所可以访问B的方法,就和A中的私有变量形成了闭包。此时B已经捕获了A中的变量。即便是A对象销毁了,被B捕获的私有变量仍然不会释放。
所以可以理解,如果希望系统 方法和当前环境的上下文绑定 的话,就可以使用闭包。
尽管有些不恰当,但是我们可以再换一个实际的场景来举个例子帮助你理解:
当你在购物的时候,你选中了一件商品,然后下单。这时候你的订单就已经绑定了你选中的商品。那么你的购买行为就和商品组成了闭包。这时候即便商家下架商品,也不会影响到商品后续的发货签收。并且这些逻辑只需要根据购买行为中的信息进行判断就可以了,而不用关心商家什么时候把商品下架,这写逻辑发生时候的商品价格是什么。
结合上面的例子,我们可以发现,当一个方法和其调用的外部环境形成闭包的时候,由于外部环境已经确定,那么我们就不用再关心外部环境了,而只用关心方法本身。
所以针对我们为什么需要闭包,我给出的答案是:
使用闭包的设计方式,由于闭包本身已经包含了上下文信息,所以可以对北向功能调用(用户)屏蔽由于环境而引发的复杂处理和交互成本。
对于Java来说,可以理解为主要是两种的闭包方式:
其中内部类除了本身的内部类还有局部内部类、匿名内部类。我们以最简洁的匿名内部类来举例:
此时say方法中便捕获了length属性,而如果你使用的是足够版本的IDE的话,获取还会提示你:
Anonymous new ISay() can be replaced with lambda
替换后:
那么这时候你就会发现,此时return的对象就是一个通过匿名后直接描述的函数,而这个函数同时还关联了上下文之外的环境信息。
而在java8中lambda的中我们可以通过函数式编程接口直接接收这种闭包,这些接口常用的为:
关于函数式接口本文就不展开了,但是利用函数式接口,我们就可以这样传递一个闭包:
此时say返回的就不是String的结果了,而是直接将闭包本身返回了。而这个闭包在创建的时候就已经绑定了所需要的环境属性。所以我们可以在需要的时候再调用闭包,并且不用再关心它到底依赖了其他哪些变量:
当然你可能会角色,这个length似乎没有什么用呀,那如果我们换一个形式:
使用Spring的依赖注入后,那么这个Closure类本身可能是各种策略模式策略器中的一个,策略器返回的是一个已经关联了具体策略路由的闭包。而当这个方法提供出去的时候,后续的调用者只需要知道这个闭包是可以针对文本进行处理的就可以,而至于之前是使用的什么策略它则不用关心。因为这些细节都已经通过闭包屏蔽了。
仅仅谈论好的而对问题闭口不谈确实不好,虽然闭包提供了强大的功能,可以对业务细节进行屏蔽,对系统进行接耦拆分。但是闭包本身确实有一些问题需要留意:
可以发现,这两个问题都是由于闭包本身的优点而产生的。由于闭包关联了环境信息,所以其让环境信息中对象的生命周期变长,这对于系统性能的维护以及jvm的垃圾回收都有负面因素。而同时因为不同于一般的编码风格,闭包的使用需要开发人员对实体进行抽象,才能比较好地实现。总结来说,对于开发人员本身有一定要求。
平时我们也经常使用lambda表达式来处理一些业务逻辑,偶尔会出现一下的情况:
先不管这段代码的实现业务背景是什么,但是IDE会提示在userIds.get(i)中的i提示的信息为:
Variable used in lambda expression should be final or effectively final
结合了上文中关于闭包的内容,我们就不难理解。由于闭包是要关联外部环境变量,在这部分代码中关联的是索引变量i,但是因为i本身是局部变量,无法保证关联环境的稳定性(我自己的理解),所以java编译器会强制的要求当闭包关联的是局部变量的时候,需要添加final关键字,而在进行final关键字从而保证该变量的内存引用不会发生改变。从本章的代码上结论上看则是进行了一次内存拷贝,来保证每个闭包中关联的环境变量不会改变,修改后的代码为:
闭包本身是一种面向抽象编程,屏蔽细节的设计原则。在良好的设计下,可以通过闭包来屏蔽对于环境信息的感知,从而简化外部对于系统理解的成本,提高系统的易用性。