闭包(closure),是一种编程语言特性,它指的是代码块和作用域环境的结合,早期由scehme语言引入这一特性,随后几乎所有语言都带有这一特性,典型的闭包如下:
(define (func a)
(lambda (b)
(lambda (c) (+ a b c))))
(((func 1) 2) 3)
闭包里的自由变量会绑定在代码块上,在离开创造它的环境下依旧生效,而这一点使用代码块的人可能无法察觉。
闭包里的自由变量的形式有很多,先举个简单例子。
function add(one){
return function(two){
return one+two;
}
}
const a = add(1);
const b = add(2);
a(1);//2
b(1);//3
在上面的例子里,a和b这两个函数,代码块是相同的,但若是执行a(1)和b(1)的结果却是不同的,原因在于这两者所绑定的自由变量是不同的,这里的自由变量其实就是函数体里的one。add这个函数嵌套返回一个新的函数,而新的函数也带来了新的作用域,在JS里,自由变量的查找会从本级作用域依次向外部作用域,直到查到最近的一个,而自由变量的绑定也会在函数定义的时候就已经确定,这也是词法作用域(或称静态作用域)的具体表现。自由变量的引入,可以起到和OOP里的封装同样作用,我们可以在一层函数里封装一些不被外界知晓的自由变量,从而达到相同的效果,举例说这么一个简单的java类。
class Demo{
private int r;
private int k = 1;
public Demo(int r){
this.r = r;
}
public int getSquare(){
return this.r*this.r*this.k;
}
public void incr(){
this.k++;
}
}
这里的变量r被封装了,我们可以new Demo(1)或者new Demo(2)返回不同的实例,然后调用相同的方法来得到不同的结果,这一点如果用自由变量也可以做到。
function demo(r){
let k = 1;
return {
getSquare:function(){
return r*r*k;
},
incr:function(){
k++;
}
}
}
在执行demo(1)或者demo(2)的时候,得到的对象都可以用来执行相同的方法,然而他们的自由变量(r和k)都是相互隔离的,这就是封装的表现。自由变量的确定在其他语言有着不一样的表现,比如说php里,函数与函数之间的作用域是完全隔离的,除非你用传参或者global来拿到外部作用域的变量,这会导致我们做封装的时候极为麻烦,所以php5.3里加了use语法,它允许在函数作用域里引用上一层的自由变量。比如上面的JS代码可以改成这样的php代码。
<?php
function demo($r){
$k = 1;
return array(
"getSquare"=>function() use ($r,&$k){
return $r*$r*$k;
},
"incr"=>function() use (&$k){
$k++;
}
);
}
这里还有一个要注意的地方就是在use $k的时候,用了&表示按引用传递,因为如果不这么做的话,内部函数里的这个$k实际上只是一份值拷贝,无法改变其值,也无法应用改变之后的新值。有人比较偏爱php 的use语法,因为这样可以明确的确定需要使用的外部自由变量,而有的人偏爱js这种隐式写法,原因是写起来简洁不累赘,只能说语言设计都有不同的取舍。刚才说到的这些,其实函数都是一等公民的情况下,然而在其他形式的语言里,其实也都有闭包,比如说在java里,虽然无法定义一个脱离于class的函数,但是我们可以在method里的内部定义一个class,这个class也就是local class,它实际上就是一种闭包,举例来说。
class Demo {
private volatile int a;
public void test(final int b) {
new Thread(
new Runnable() {
void run() {
a++;
System.out.print(b);
}
}
).start();
}
}
上面test方法里的local class,可以直接引用或者更改定义在类里的private variable,也可以读取方法里的参数,并且它的自由变量绑定也是在定义的时候就已经确定好的。然而由于java本身的限制,所以上面的参数b必须是final的,这一点在java8的lambda也不例外,就算在java8里你不使用final确定,它还是隐式的认为其是final,所以无法在local class里的方法更改这个参数。再说说C++,C++里最开始是使用运算符重载来达到定义函数类型,但是它有一个缺点就是无法捕获外部的自由变量,为了达到相同的效果,你需要绕很多圈子,在C++11里引入的lambda expression特性终于可以轻易的使用闭包了。
void find(string a) {
int size;
vector<string> arr;
auto i = std::find_if(arr.begin(), arr.end(),
[&](const string& s) {
return s != a && s.size() > size;
}
);
}
在上面的& {}语句里,我们可以使用外部的自由变量a和size,这一点可以极大方便我们在C++里使用函数。然而在C语言里,我们想要做同样的事情就很困难了,C语言并不支持高阶函数,我们想要让函数能作为参数代入,或者让函数能够返回函数,我们需要使用函数指针,典型的函数指针是这样子的:
int (*funcP) (int,int);
(*funcP)(1,2);
这里的funcP本质上是一个指针,所以它可以在C语言里被函数当做参数或者当做返回值,然而它无法代入自由变量,也就是说它根本没法做到捕获作用域变量,我们如果想要使用外层变量,必须手动加入一个参数的指针,然后再和函数指针一起代出去,这样才能使用到外层的变量。何其麻烦!所以很多厂商为c语言定制了闭包特性,其中比较有名的就是苹果家的block,它的定义形式和函数指针极为相似,只不过把*换成了^,然而它却有闭包的特性,可以捕获自由变量,举例来说:
int a =10;
int main(void){
int (^op) (int);
int b = 20;
static c = 30;
op = ^(int one){ return one+a+b+c;};
op(1);
}
我们上面的op是一个block,在它的内部,可以捕获到全局变量a,以及局部变量b,静态变量c,对于全局变量a以及局部静态变量c,它是可以直接访问并且可以修改的,然而对于局部变量b,它却只能访问,而无法做修改,并且当b的值发生变化的时候,它也无法感知,实际上是因为捕获的时候就已经把b的值代入进栈的block object里了。为了能够更好的捕获自由变量,所以block还引入了一个特殊的修饰符,也就是__block,用于修饰局部非静态变量,被__block修饰的变量是可以在block里读取并修改的,它的值是动态生成的,实质上是每次执行的时候都会去获取被修饰变量的内存区域,从而达到共享变量值的效果。block object还有一个比较重要的地方就是它和其他变量一样,生命周期在定义的函数执行结束之后也就结束了,这样我们需要考虑的就是如何脱离创造它的环境下依旧有效,block引入了Block_copy这一工具函数,用于将栈上的block复制到堆上,这样新的block就可以脱离原有的创造环境了。总之,闭包在各种语言上有着不同的语法语义,其核心要素就是在于自由变量如何捕获,我们在使用闭包的时候需要注意到语言的作用域方式,以及自由变量捕获方式这些特点。
Web最新资讯,请关注微信公众号“一起玩前端”或扫描二维码关注.
著作权归作者所有。
商业转载请联系作者获得授权,非商业转载请注明出处。
作者:李引证
链接:http://zhuanlan.zhihu.com/browsnet/20658538
来源:知乎
网友评论