Contents
  1. 1. 概念
  2. 2. 意义
  3. 3. 例1
  4. 4. 例2
  5. 5. 例3
  6. 6. 作用

作为一个Javaer,在学习Javascript和Python时老碰到的一个词——闭包。在介绍闭包的概念的基础上,文本以3个详尽的例子来分析闭包的意义和作用。主要参考了IBM的一篇文章,并以JS进行论证实验。

概念

闭包函数引用环境组成的整体。

  • 闭包不是函数,只是行为和函数相似,不是所有被传递的函数都需要转化为闭包,只有引用环境可能发生变化的函数才需要这样做。
  • 闭包在运行时可以有多个实例,不同的引用环境和相同的函数组合可以产生不同的实例。
  • 所谓引用环境是指在程序执行中的某个点所有处于活跃状态的约束所组成的集合。其中的约束是指一个变量的名字和其所代表的对象之间的联系。
  • 对象是附有行为的数据,而闭包是附有数据的行为。

意义

为什么要把引用环境与函数组合起来?
因为在支持嵌套作用域的语言中,有时不能简单直接地确定函数的引用环境

一个编程语言需要哪些特性来支持闭包呢,下面列出一些比较重要的条件:

  • 函数是一阶值(First-class value),即函数可以作为另一个函数的返回值或参数,还可以作为一个变量的值;
  • 函数可以嵌套定义,即在一个函数内部可以定义另一个函数;
  • 可以捕获引用环境,并把引用环境和函数代码组成一个可调用的实体;
  • 允许定义匿名函数;
    这些条件并不是必要的,但具备这些条件能说明一个编程语言对闭包的支持较为完善。

如Javascript、Python都已一定程度支持闭包,而不支持闭包的语言如Java(8之前)。

例1

以Javascript实现一个简单计数,准确地来说是计数模板,这就需要一个初始为0的计数器和计数函数,计数函数作为模板函数的返回值。来自闭包的概念、形式与应用的例子。

1
2
3
4
5
6
7
8
9
10
11
12
function make_counter(){
var count = 0 //局部变量
function inc_count(){
count = count + 1
return count
}
return inc_count
}
c1 = make_counter()
c2 = make_counter()
c1()
c2()

如果没有闭包或者是新手,可能会觉得这段代码存在以下的矛盾:

  1. count是局部变量,作用域在make_counter()之内,调用make_counter()返回一个函数后,count应当失效
  2. make_counter()的返回值是函数inc_count(),并赋给了变量,在实际调用时执行该函数,还需要访问count

其中2正是我们的本意,需要count依旧有效。闭包的机制能使得在外部函数make_counter()返回后,内存依然保留内部函数inc_count()需要的count。如闭包实例c1,包含了函数inc_count()和相应的变量count。

闭包的一个作用是可以模拟面向对象编程。用闭包的Javascript相当于实现了这样的Java程序,计数模板产生计数函数,计数器互相独立:

1
2
3
4
5
6
7
8
9
10
11
12
public class Counter{
private int count=0;
public inc_count(){
this.count++;
}
public static void main(String[] args){
Counter c1=new Counter();
Counter c2=new Counter();
c1.inc_count();
c2.inc_count();
}
}

而改写的Javascript的面向对象写法(参考 JS面向对象的程序设计JS 中面向对象的5种写法):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function make_counter(){
this.count=0
}
make_counter.prototype = {
constructor: make_counter(),
inc_count:function(){
this.count=this.count+1
return this.count
}
}
c1=new make_counter()
c2=new make_counter()
c1.inc_count()
c2.inc_count()

这里特别废话一下,还有两种写法虽然看起来很舒服,但其实是有问题的:

1
2
3
4
5
6
7
8
9
10
11
12
function make_counter(){
this.count=0;
this.inc_count=function(){
this.count=this.count+1;
return this.count
}
}
c1=new make_counter()
c2=new make_counter()
c1.inc_count()
c2.inc_count()
c1.inc_count==c2.inc_count //false,对象的属性指向函数时,会重复的创建函数实例

相当于this.inc_count=new function(){……},而好的当然是令不同的对象共享函数块。

1
2
3
4
5
6
7
8
make_counter={
"count":0,
"inc_count":function(){
this.count=count+1;
return this.count
}
}
make_counter.inc_count()

简直不能算面向对象,让它做一个安静的美男子……

例2

以Javascript实现对1、2、……、9做连续运算,加法函数作为运算函数的传入参数,累加器初始为0。也是来自闭包的概念、形式与应用的例子。

1
2
3
4
5
6
7
8
9
10
function do10times(fn){
for(var i=0;i<10;i++){
fn(i)
}
}
sum = 0
function addsum(i){
sum = sum + i
}
do10times(addsum)

如果没有闭包或者是新手,可能会觉得这段代码存在以下的矛盾:

  1. 函数do10times()在变量sum之前已经定义,不在sum的作用域内,调用do10times()时不能访问sum
  2. 函数addsum()作为参数传给do10times(),并在do10times()中被调用10次,需要访问变量sum

其中2正是我们的本意,需要访问sum。闭包的机制能使得在参数函数addsum()携内存中的sum一起投奔do10times()。闭包实例包含了函数addsum()和相应的变量sum。

如果觉得全局变量sum不太顺眼,也就

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function do10times(fn){
for(var i=0;i<10;i++){
console.log(fn(i))
}
}
function calculator(){
var sum = 0
function addsum(i){
sum = sum + i
return sum
}
return addsum
}
c1=new calculator()
c2=new calculator()
do10times(c1)
do10times(c2)

由于对Javascript不太了解,不太清楚该如何改成面向对象的形式,另外在JS中this指代的对象需要根据运行时此函数在什么对象上被调用判断,具体参考 js 中的this,constrct ,prototype详解js中this的四种调用模式。由于本文重点并不在此,就不多加研究了。

例3

闭包引用循环变量时要特别注意。来自廖雪峰Python2.7教程的例子,比较两例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function count(){
var fs = new Array()
for(var i=0;i<4;i++){
function f(){
return i*i
}
fs[i]=f
}
return fs
}
ff = count()
ff[0]() //16
ff[1]() //16
ff[2]() //16
ff[3]() //16

调用函数count(),循环变量i从0逐步变为4,返回4个函数f(),调用f()时访问i已是4。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function count(){
var fs = new Array()
for(var i=0;i<4;i++){
function g(j){ //保留j
function f(){
return j*j
}
return f
}
fs[i]=g(i)
}
return fs
}
f1 = count()
f1[0]() //0
f1[1]() //1
f1[2]() //4
f1[3]() //9

调用函数count(),循环变量i从0逐步变为4,且每一步都把i的值绑定给函数g的参数j,g(j)又返回f(),最终返回4个函数f(),调用f()时各自保留有不同的参数变量j。

对比图如下所示。
1.png

另外还有个比较经典的例子,也是这个道理:

1
2
3
4
5
for(var i=0;i<4;i++)
setTimeout(function(){console.log(i)},0) //输出4,4,4,4

for(var i=0;i<4;i++)
setTimeout(function(j){console.log(j)}(i),0) //输出0,1,2,3

很棒的总结!

返回闭包时牢记的一点就是:返回函数不要引用任何循环变量,或者后续会发生变化的变量

如果一定要引用循环变量怎么办?方法是再创建一个函数,用该函数的参数绑定循环变量当前的值,无论该循环变量后续如何更改,已绑定到函数参数的值不变


作用

闭包的概念、形式与应用一文提到了三个作用:

  • 加强模块化
  • 抽象
  • 简化代码

个人感觉,闭包的确可以给语言增添不少活力,也可能不小心掉入坑中……

Contents
  1. 1. 概念
  2. 2. 意义
  3. 3. 例1
  4. 4. 例2
  5. 5. 例3
  6. 6. 作用