ModernPHP 闭包

本文介绍PHP中的闭包和匿名函数的概念,以及它们的具体使用。

0x00 简介

闭包和匿名函数是PHP5.3.0中引入的特性。下面介绍下这两个概念的定义,可能看上去会比较难理解,但是后面会给出实例可以更好的掌握。

闭包指的是在创建时封装周围状态的函数。即便闭包所在的环境不存在了,闭包中封装的状态依然存在。

匿名函数其实是没有名字的函数,匿名函数可以赋值给变量,也能像对象那样被传递,常常被用作函数或者方法的回调。

理论上讲,闭包和匿名函数是不同的概念,但是PHP将其是做相同的概念。PHP闭包的语法和普通函数相同,但是其实它是伪装成函数的对象,事实上是Closure类的实例。

0x01 闭包的创建

1
2
3
4
5
6
$closure = function($name) {
return sprintf('Hello %s', $name);
}

echo $closure("Josh");
// 输出 Hello Josh

我们之所以能够调用$closure变量,是因为这个变量的值是一个闭包,而且闭包对象实现了__invoke()魔术方法。只要变量名后有(),PHP就会去查找并调用__invoke()方法。

很多的PHP函数会用到回调函数,比如array_map()和preg_replace_callback()函数。下面的例子给出了将闭包对象作为回调参数传给array_map()函数。

1
2
3
4
5
6
$numberPlusOne = array_map(function($nunmber) {
return $number + 1;
}, [1, 2, 3]);

print_r($numberPlusOne);
// 输出 [2, 3, 4]

0x01 附加状态

如果你了解JavaScript的闭包,你应该知道JavaScript的闭包会自动封装引用的状态。但在PHP中并不会,你必须手动调用闭包对象的bindTo()方法或者使用use关键字,把状态附加到PHP闭包中。

使用use关键字附加状态常见得多,下面给出了例子,use关键字将变量附加到闭包时,附加的变量会记住附加时赋予它的值。

1
2
3
4
5
6
7
8
9
10
11
12
function enclosePerson($name) {
return function ($doCommand) use ($name) {
return sprintf('%s, %s', $name, $doCommand);
}
}

// 将字符串"Clay"封装在闭包中
$clay = enclosePerson("Clay");

// 传入参数,调用闭包
echo $clay('go away!');
// 输出 "Clay, go away!"

上述例子中,enclosePerson函数有一个参数$name,这个函数返回一个闭包对象,并且在闭包对象中封装了$name参数。即便返回的闭包对象跳出了enclosePerson()函数的作用域,$name参数的值还是能获取到,因为$name变量仍在闭包中。

上面提到过,PHP闭包是对象,它有一个bindTo方法,这个方法可以把Closure对象的内部状态绑定到其他对象上。bindTo的第二个参数很重要,其作用是指定绑定闭包的那个对象所属的PHP类。因此闭包可以访问闭包对象中受保护和私有的成员变量。

PHP框架经常使用bindTo方法把路由URL映射到匿名函数上。框架会把匿名函数绑定到应用对象上,这么做可以在这个匿名函数中使用$this关键字引用重要的应用对象,下面给出例子。

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
class App 
{
protected $routes = [];
protected $responseStatus = "200 OK";
protected $responseContentType = "text/html";
protected $responseBody = "Hello world";

public function addRoute($routePath, $routeCallback)
{
$this->routes[$routePath] = $routeCallback->bindTo($this, __CLASS__);
}

public function dispatch($currentPath)
{
foreach ($this->routes as $routePath => $callback) {
if($routePath === $currentPath) {
$callback();
}
}

header('HTTP/1.1 ' . $this->responseStatus);
header('Content-type: ' . $this->responseContentType);
header('Content-length: ' . mb_strlen($this->responseBody));
echo $this->responseBody;
}
}

App类中的addRoute()方法,它有2个参数,分别是路由路径(例如/users/josh)字符串和一个路由回调。dispatch()方法的参数是当前HTTP请求的路径,它会调用匹配的路由回调。第10行是重点,我们把回调函数绑定在了当前的App实例中,这么做就能在回调函数中处理App实例的状态。

1
2
3
4
5
6
$app = new App();
$app->addRoute('/users/josh', function() {
$this->responseContentType = 'application/json;charset=utf8';
$this->responseBody = json_encode(['name'=> 'josh']);
});
$app->dispatch('/users/josh');

ModernPHP 系列全集:传送门

0%