首页 | 新闻 | 新品 | 文库 | 方案 | 视频 | 下载 | 商城 | 开发板 | 数据中心 | 座谈新版 | 培训 | 工具 | 博客 | 论坛 | 百科 | GEC | 活动 | 主题月 | 电子展
返回列表 回复 发帖

函数式编程中的重要概念(6)闭包

函数式编程中的重要概念(6)闭包

闭包闭包(closure)是函数式编程相关的一个重要概念,也是很多开发人员比较难以理解的概念。很多编程语言都有闭包或类似的概念。
在上一篇文章介绍 λ 演算的时候提到过 λ 项的自由变量和绑定变量,如 λx.x+y 中的 y        就是自由变量。在对λ项求值时,需要一种方式可以获取到自由变量的实际值。由于自由变量不在输入中,其实际值只能来自于执行时的上下文环境。实际上,闭包的概念来源于 1960        年代对 λ 演算中表达式求值方式的研究。
闭包的概念与高阶函数密切相关。在很多编程语言中,函数都是一等公民,也就是存在语言级别的结构来表示函数。比如 Python 中就有函数类型,JavaScript 中有 function        关键词来创建函数。对于这样的语言,函数可以作为其他函数的参数,也可以作为其他函数的返回值。当一个函数作为返回值,并且该函数内部使用了出现在其所在函数的词法域(lexical        scope)的自由变量时,就创建了一个闭包。我们首先通过一段简单的 JavaScript 代码来直观地了解闭包。
清单 8 中的函数 idGenerator 用来创建简单的递增式的 ID 生成器。参数 initialValue 是递增的初始值。返回值是另外一个函数,在调用时会返回并递增        count 的值。这段代码就用到了闭包。idGenerator 返回的函数中使用了其所在函数的词法域中的自由变量 count。count        不在返回的函数中定义,而是来自包含该函数的词法域。在实际调用中,虽然 idGenerator 函数的执行已经结束,其返回的函数 genId 却仍然可以访问 idGenerator        词法域中的变量 count。这是由闭包的上下文环境提供的。
清单 8. JavaScript        中的闭包示例
1
2
3
4
5
6
7
8
9
10
function idGenerator(initialValue) {
let count = initialValue;
return function() {
       return count++;
};
}

let genId = idGenerator(0);
genId(); // 0
genId(); // 1




从上述简单的例子中,可以得出来构成闭包的两个要件:
  • 一个函数
  • 负责绑定自由变量的上下文环境
函数是闭包对外的呈现部分。在闭包创建之后,闭包的存在与否对函数的使用者是透明的。比如清单 8 中的        genId        函数,使用者只需要调用即可,并不需要了解背后是否有闭包的存在。上下文环境则是闭包背后的实现机制,由编程语言的运行时环境来提供。该上下文环境需要为函数创建一个映射,把函数中的每个自由变量与闭包创建时的对应值关联起来,使得闭包可以继续访问这些值。在        idGenerator 的例子中,上下文环境负责关联变量 count 的值,该变量可以在返回的函数中继续访问和修改。
从上述两个要件也可以得出闭包这个名字的由来。闭包是用来封闭自由变量的,适合用来实现内部状态。比如清单 8 中的        count 是无法被外部所访问的。一旦 idGenerator 返回之后,唯一的引用就来自于所返回的函数。在 JavaScript 中,闭包可以用来实现真正意义上的私有变量。
从闭包的使用方式可以得知,闭包的生命周期长于创建它的函数。因此,自由变量不能在堆栈上分配;否则一旦函数退出,自由变量就无法继续访问。因此,闭包所访问的自由变量必须在堆上分配。也正因为如此,支持闭包的编程语言都有垃圾回收机制,来保证闭包所访问的变量可以被正确地释放。同样,不正确地使用闭包可能造成潜在的内存泄漏。
闭包的一个重要特性是其中的自由变量所绑定的是闭包创建时的值,而不是变量的当前值。清单 9 是一个简单的 HTML 页面的代码,其中有 3 个按钮。用浏览器打开该页面时,点击 3        个按钮会发现,所弹出的值全部都是 3。这是因为当点击按钮时,循环已经执行完成,i 的当前值已经是 3。所以按钮的 click 事件处理函数所得到是 i 的当前值 3。
清单 9.        闭包绑定值的演示页面
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<!DOCTYPE html>
<html lang="en">
<head>
   <title>Test</title>
</head>
<body>
   <button>Button 1</button>
   <button>Button 2</button>
   <button>Button 3</button>
</body>
<script>
   var buttons = document.getElementsByTagName("button");
   for (var i = 0; i < buttons.length; i++) {         
     buttons.addEventListener("click", function() {
       alert(i);              
     });
   }
</script>
</html>




如果把 JavaScript 代码改成清单 10 所示,就可以得到所期望的结果。我们创建了一个匿名函数并马上调用该函数来返回真正的事件处理函数。处理函数中访问的变量 i        现在成为了闭包的自由变量,因此 i 的值被绑定到闭包创建时的值,也就是每个循环执行过程中的实际值。
清单 10.        使用闭包解决绑定值的问题
1
2
3
4
5
6
7
8
var buttons = document.getElementsByTagName("button");
for (var i = 0; i < buttons.length; i++) {         
   buttons.addEventListener("click", function(i) {
      return function() {
        alert(i);              
      }
    }(i));
}




在 Java 中有与闭包类似的概念,那就是匿名内部类。在匿名内部类中,可以访问词法域中声明为 final 的变量。不是 final        的变量无法被访问,会出现编译错误。匿名内部类提供了一种方式来共享局部变量。不过并不能对该变量的引用进行修改。在清单  11 中,变量 latch        被两个匿名内部类所使用。
清单 11. Java        中的匿名内部类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class InnerClasses {

  public static void main(String[] args) {
    final CountDownLatch latch = new CountDownLatch(1);

    final Future<?> task1 = ForkJoinPool.commonPool().submit(() -> {
      try {
        Thread.sleep(ThreadLocalRandom.current().nextInt(2000));
      } catch (InterruptedException e) {
        e.printStackTrace();
      } finally {
        latch.countDown();
      }
    });

    final Future<?> task2 = ForkJoinPool.commonPool().submit(() -> {
      final long start = System.currentTimeMillis();
      try {
        latch.await();
      } catch (InterruptedException e) {
        e.printStackTrace();
      } finally {
        System.out.println("Done after " + (System.currentTimeMillis()
- start) + "ms");
      }
    });

    try {
      task1.get();
      task2.get();
    } catch (InterruptedException | ExecutionException e) {
      e.printStackTrace();
    }
  }
}




可以被共享的变量必须声明为 final。这个限制只对变量引用有效。只要对象本身是可变的,仍然可以修改该对象的内容。比如一个 List 类型的变量,虽然对它的引用是 final        的,仍然可以通过其方法来修改 List 内部的值。
返回列表