什么是闭包
1、一个含有自由变量的函数;
2、这些自由变量所在的环境。
也就是函数和环境的总和构成一个闭包。外部环境持有内部函数所使用的自由变量,对内部函数形成“闭包”。
简单但不严格的说,一个函数的“自由变量”就是既不是参数也不是局部变量的变量(外部变量)。
一个纯粹(无副作用)的函数如果不含有自由变量,那么每次用相同的参数调用后的得到的结果肯定是一样的。但如果一个函数含有自由变量,那么调用返回的结果不但依赖于参数的值,还依赖于自由变量的值。因此一个含有自由变量的函数要正确执行,必须保证其所依赖的外围环境的存在。
在JS中的样子如下
function a() {
var i = 0;
function b() { alert(++i); }
return b;
}
var c = a();
c(); //1
c(); //2
闭包和面向对象
闭包与对象是从两个完全不同的角度描述了一件事情:一段代码与其环境的关系。
所以可以用对象来模拟(实现)闭包,也可以用闭包来模拟(实现)对象。
刚刚闭包里的例子的效果完全可以用对象做出来, 这两种方式都体现了函数和环境之间的关系。
class a {
private int i = 0;
int b( ) { System.out.priintly(++i); }
}
a c = new a();
c.b //1
c.b //2
当然严格来说方法所捕获的自由变量不是i,而是this;i是通过this来访问到的,完整写出应该是this.i。
闭包的传值
让我们考察下面两种情况:
只有值捕获(capture-by-value):只需要在创建闭包的地方把捕获的值拷贝一份到对象里即可。Java的匿名内部类和Java 8新的lambda表达式都是这样实现的。
有引用捕获(capture-by-reference):把被捕获的局部变量“提升”(hoist)到对象里。C#的匿名函数(匿名委托/lambda表达式)就是这样实现的。参考Eric Lippert大神对“hoist”一词的讲解。不要把这个“hoist”的用法跟JavaScript里说的把局部变量提前到函数开头来声明的那种用法混为一谈。
如果变量(variable)是不可变(immutable)的,那么使用者无法感知值捕获和引用捕获的区别。
- 有些语言(例如C++11)允许显式指定捕获列表以及捕获方式(值捕获还是引用捕获),这样最清晰,不过写起来比较长;
- 有些语言(例如JavaScript)只有引用捕获,要模拟值捕获的效果需要手动新建闭包和局部变量;有些语言(例如C#)对不可变量(const local)做值捕获,对普通局部变量做引用捕获;由于无法感知对不可变量的值捕获与引用捕获的区别,统一把这个行为描述成是引用捕获更方便一些。
- 有些语言(例如Java)虽然目前只实现了值捕获,但是还要留着面子不承认自己只做了值捕获,所以只允许捕获不变量(final local),或者例如Java 8允许捕获事实上不变量(effectively final local)。这样,虽然实现用的是值捕获,但效果看起来跟引用捕获一样;就算以后的Java扩展到允许通用的(对可变变量的)引用捕获,也不会跟已有的代码发生不兼容。
public class Test {
public static void main(String[] args) {
}
public void test(final int b) {
final int a = 10;
new Thread(){
public void run() {
System.out.println(a);
System.out.println(b);
};
}.start();
}
}
- 有些语言(例如Python)的lambda略奇葩,实现的是引用捕获,但是lambda内不能对捕获的变量赋值,只有原本定义那些变量的作用域里能对它们赋值。