浅谈php闭包/匿名函数

闭包(Closure)是我们在编程中经常遇到的一个概念,但闭包究竟是什么?又有什么用呢?

在编程中,闭包与匿名函数(又名lambda表达式)其实是不同的概念,但两者经常同时使用。在 PHP 中,二者的概念不做区分,下文均以闭包指代。

I. 闭包是什么?

闭包的常用类型

闭包又名匿名函数,也就是没有函数名称的函数。我们先来看下常用类型

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
// 定义一个闭包,并把它赋给变量 $closure
$closure = function () {
return 7;
}

// 使用闭包也很简单
$closure(); //这样就调用了闭包,输出 7

// 如果要即时调用闭包,我们也可以这样写
(function () {
echo 7;
})();

// 当然更多的时候是把闭包作为参数(回调函数)传递给函数
function testClosure (Closure $callback) {
return $callback();
}

// $closure 作为参数传递给函数 testClosure,如果是普通函数是没有办法作为testClosure的参数的
testClosure($closure);

// 也可以直接将定义的闭包作为参数传递,而不用提前赋给变量
testClosure (function () {
return 7;
});

// 闭包不止可以做函数的参数,也可以作为函数的返回值
function getClosure () {
return function () { return 7; };
}

$c = getClosure(); // 函数返回的闭包就复制给 $c 了
$c(); // 调用闭包,返回 7

闭包类

定义一个闭包函数,其实是产生了一个闭包类(Closure)的对象,Closure 类摘要如下:

1
2
3
4
5
6
7
8
9
10
Closure {
// 用于禁止实例化的构造函数
__construct ( void )

// 复制一个闭包,绑定指定的$this对象和类作用域
public static bind ( Closure $closure , object $newthis [, mixed $newscope = 'static' ] ) : Closure

// 复制当前闭包对象,绑定指定的$this对象和类作用域
public bindTo ( object $newthis [, mixed $newscope = 'static' ] ) : Closure
}

我们可以通过var_dump($c instanceof Closure);看到,闭包确实是Closure的一个实例,通过var_dump(is_callable($c));看到,闭包是Callable的数据类型。

Closure::bind() 实例

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
36
37
38
39
40
41
42
43
44
class Animal {  
private static $cat = "cat";
private $dog = "dog";
public $pig = "pig";
}

/*
* 获取Animal类静态私有成员属性
*/
$cat = static function() {
return Animal::$cat;
};

/*
* 获取Animal实例私有成员属性
*/
$dog = function() {
return $this->dog;
};

/*
* 获取Animal实例公有成员属性
*/
$pig = function() {
return $this->pig;
};

$bindCat = Closure::bind($cat, null, new Animal());// 给闭包绑定了Animal实例的作用域,但未给闭包绑定$this对象。这是由于静态闭包不能有绑定的对象( newthis 参数的值应该设为 NULL)(详情请查看手册[Closure::bindTo页面文档](https://www.php.net/manual/zh/closure.bindto.php))

$bindCat = Closure::bind($cat, new Animal(), 'Animal');// 若$cat不是静态闭包时有效

$bindDog = Closure::bind($dog, new Animal(), 'Animal');// 给闭包绑定了Animal类的作用域,同时将Animal实例对象作为$this对象绑定给闭包

$bindPig = Closure::bind($pig, new Animal());// 将Animal实例对象作为$this对象绑定给闭包,保留闭包原有作用域

echo $bindCat(),PHP_EOL;// 根据绑定规则,允许闭包通过作用域限定操作符获取Animal类静态私有成员属性

echo $bindDog(),PHP_EOL;// 根据绑定规则,允许闭包通过绑定的$this对象(Animal实例对象)获取Animal实例私有成员属性

echo $bindPig(),PHP_EOL;// 根据绑定规则,允许闭包通过绑定的$this对象获取Animal实例公有成员属性

// bindTo与bind类似,是面向对象的调用方式,这里只举一个,其他类比就可以
$bindCat = $cat->bindTo(null, 'Animal');// 同样的,静态闭包不能绑定对象,因此$newthis为 null
echo $bindCat(); // cat

如果未能指定作用域的范围,绑定后的闭包只能访问public属性的值。

Closure::bindTo() 实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class A {
function __construct($val) {
$this->val = $val;
}

function getClosure() {
//returns closure bound to this object and scope
return function() { return $this->val; };
}
}

$ob1 = new A(1);
$ob2 = new A(2);

$cl = $ob1->getClosure();
echo $cl(), PHP_EOL;// 1
$cl = $cl->bindTo($ob2);// 它与当前对象的函数体相同、绑定了同样变量,但可以绑定不同的对象,也可以绑定新的类作用域
echo $cl(), PHP_EOL;// 2

II. 闭包有什么用?

1. 储存变量

1
2
3
4
5
6
7
8
9
10
11
function makeHelloWorld($name) { 
$i = 0;
return function() use ($name, &$i) {
echo $name; $i++; echo $i."\n"; };
}
$hello1 = makeHelloWorld("wuying");
$hello2 = makeHelloWorld("wuying1");
$hello1(); //wuying1
$hello1(); //wuying2
$hello1(); //wuying3
$hello2(); //wuying11

在这个示例中,$hello1 = makeHelloWorld("wuying"); 的作用是将参数wuying1传递给makeHelloWorld(),并接收其中的闭包作为返回值,因此,$hello1便成为了闭包,当我们执行$hello1()时,闭包开始执行。由于引用了闭包外的变量$i,所以每次的执行都修改了$i,所以最后依次输出了wuying1wuying2wuying3

那么,为什么闭包可以存储变量呢?这是因为在没有闭包的语言中,变量的生命周期只限于创建它的环境。但在有闭包的语言中,只要有一个闭包引用了这个变量,它就会一直存在。

另一种在局部函数中储存变量的方式是使用static关键字,其原理是将变量存储于静态存储区,而非普通变量存储的栈区,详见另一篇文章浅谈PHP中的static关键字

2. 延时执行函数

比如,我们在 JavaScript 中每隔 1 秒钟输出一个随机数

1
setInterval(function(){console.log(Math.random())}, 1000)

在这个例子中,我们对闭包进行了多次调用,而且不是调用时即时执行。

3. 对处理逻辑封装,形成更好的一体封装

通常我们在调用函数时,传入的是参数是数据,那么只能通过参数对函数的结果进行控制,无法控制其过程,而匿名函数的存在可以作为参数传给函数,也可以作为变量赋值,进而控制函数的执行过程,因此,匿名函数的引入增强了程序编写的灵活性,可以实现更加高效的设计方案。

如果在有闭包前,我们需要单独创建具名函数,然后使用名称引用这个函数

1
2
3
4
5
6
<?php
$func = function($value) {
return $value * 2;
};

print_r(array_map($func, range(1, 5)));

但这样把回调与使用分离,而闭包的出现就很好的解决了这个问题。

而由于闭包的封装性,闭包内无法访问包外变量,但我们可以通过use关键字来引入外部变量,如:

1
2
3
4
$quantity = 10;
function ($price) use ($quantity) {
return $price * $quantity;
}

再来一个例子,使用闭包打印斐波那契数列。我们知道斐波那契数列有下面的规律:

1
2
3
4
# f(n) 表示数列中第 n 个数的值
f(n) = 0; (n = 0)
f(n) = 1; (n = 1)
f(n) = f(n-1)+f(n-2); (n >= 2)

使用递归的方法

1
2
3
4
5
6
7
8
9
<?php
function fibonacci(int $n): int {
if ($n < 2) {
return $n;
}
return fibonacci($n-1) + fibonacci($n-2);
}

echo fibonacci(10), "\n"; // 55

如果打印数列,那每一次都需要重复计算前面已经计算过的数据(只需要前两个就好)。可以使用闭包保存上一次的运行环境。

php 版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
$fibonacci = function (): callable {
$x = 0;
$y = 1;
return function () use (&$x, &$y): int {
list($x, $y) = [$y, $x+$y];
return $x;
};
};

$f = $fibonacci();

for ($i = 0; $i < 10; $i++) {
echo $f() , "\n";
}

js 版本

1
2
3
4
5
6
7
8
9
10
11
12
13
let fibonacci = _ => {
let x = 0, y = 1;
return _ => {
[x, y] = [y, x+y];
return x;
};
};

let f = fibonacci();

for (let i = 0; i <= 10; i++) {
console.log(f());
}

总结

合理使用闭包,能够让我们的代码更加清晰、更加灵活。


参考资料

  1. 闭包和匿名函数 - 学院君
  2. 闭包的编程思想 - B站
  3. 闭包-维基百科
  4. Closure类 - PHP手册
  5. Callable - PHP手册
  6. PHP 闭包(Closure)- LearKu
  7. 学习一下闭包函数 - LearKu
因为热爱,所以执着。