PHP中的闭包

思考并回答以下问题:

  • 闭包是个Closure类吗?
  • 闭包在创建时就会为传入的参数、use中的变量创建新的内存,相当于clone一份。怎么理解?
  • Closure的bind方法是什么意思?
  • 可以在匿名函数中使用$this关键字吗?

在PHP中闭包与匿名函数是一个概念:

1
2
3
$sayHello = function($name){
echo("Hello $name");
};

在PHP中闭包是像函数的对象,是个Closure类,只是能够像函数一样调用:

1
$sayHello("world");

输出:

1
Hello world

1
var_dump($sayHello instanceof Closure);

输出:

1
boolean(true)

闭包在创建时就会为传入的参数、use中的变量创建新的内存,相当于clone一份,因此与函数外部的变量变化无关;

1
2
3
4
5
6
7
8
9
10
11
$name = 'world';

$sayHello = function() use($name){
echo("Hello $name");
}; // 这里不要忘记结束的;号

$sayHello(); // 必须函数方式调用,即用(), 输出Hello world

$name = 'zj';

$sayHello();// 输出Hello world

如果想使闭包内和外部的变量同步,则use中传入引用即可,就是加个&:

1
2
3
4
5
6
7
8
9
10
11
$name = 'world';

$sayHello = function() use(&$name){ // 传入了引用
echo("Hello $name");
};//这里不要忘记结束的;号

$sayHello(); // 必须函数方式调用,即用(), 输出Hello world

$name = 'zj';

$sayHello();// 输出Hello zj

Closure类有两个方法:bind()和bindTo()

  • Closure::bind — 复制一个闭包,绑定指定的$this对象和类作用域。
  • Closure::bindTo — 复制当前闭包对象,绑定指定的$this对象和类作用域。

通过这两个方法可以给类扩展复杂功能,类似策略模式,将实际操作与类定义解耦。比如通过bing为用户类增加行为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Class User{
public $name = 'Tom';
public $age = 10;
private $action = [];

//...
}

$sayHello = function(){
echo("Hello {$this->name}\n");
};

$swimming = function(){
echo("{$this->name} is swimming\n");
};

$bindSayHello = Closure::bind($sayHello, new User());
$bindSwimming = Closure::bind($swimming, new User());
$bindSayHello();
$bindSwimming();

输出:

1
2
Hello Tom 
Tom is swimming

可以把类写的更优雅点:

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
Class user{
public $name = 'Tom';
public $age = 10;
private $action = [];

public function addAction($actionName, $actionFunction){
$this->action[$actionName] = Closure::bind($actionFunction, $this);
}

public function doAction(){
foreach ($this->action as $actionFunction) {
$actionFunction($this->name);
}
}
}

$sayHello = function(){
echo("Hello {$this->name}\n");
};

$swimming = function(){
echo("{$this->name} is swimming\n");
};

$bindSayHello = Closure::bind($sayHello, new user());
$bindSayHello();

$user = new user();

$user->addAction('sayHello', $sayHello);
$user->addAction('swimming', $swimming);
$user->doAction();
// 输出:
// Hello Tom
// Hello Tom
// Tom is swimming

bindTo方法与bind类似,只是通过闭包调用的,将自身绑定到对象或类上。

bind及bindTo方法都有第三个参数,确定绑定的作用域。

发现一个闭包实现中间件的例子,搬来作为补充学习:

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
45
46
47
48
49
50
51
<?php
// 框架核心应用层
$application = function($name) {
echo "this is a {$name} application\n";
};

// 前置校验中间件
$auth = function($handler) {
return function($name) use ($handler) {
echo "{$name} need a auth middleware\n";
return $handler($name);
};
};

// 前置过滤中间件
$filter = function($handler) {
return function($name) use ($handler) {
echo "{$name} need a filter middleware\n";
return $handler($name);
};
};

// 后置日志中间件
$log = function($handler) {
return function($name) use ($handler) {
$return = $handler($name);
echo "{$name} need a log middleware\n";
return $return;
};
};

// 中间件栈
$stack = [];

// 打包
function pack_middleware($handler, $stack)
{
foreach (array_reverse($stack) as $key => $middleware)
{
$handler = $middleware($handler);
}
return $handler;
}

// 注册中间件
// 这里用的都是全局中间件,实际应用时还可以为指定路由注册局部中间件
$stack['log'] = $log;
$stack['filter'] = $filter;
$stack['auth'] = $auth;

$run = pack_middleware($application, $stack);

输出:

1
2
3
4
Laravle need a filter middleware
Laravle need a auth middleware
this is a Laravle application
Laravle need a log middleware

打包程序

中间件的执行顺序是由打包函数(pack_middleware)决定,这里返回的闭包实际上相当于:

1
2
$run = $log($filter($auth($application)));
$run('Laravle');

编写规范
中间件要要满足一定的规范:总是返回一个闭包,闭包中总是传入相同的参数(由主要逻辑决定), 闭包总是返回句柄(handler)的执行结果;

如果中间件的逻辑在返回句柄return $handler($name)前完成,就是前置中间件,否则为后置中间件。

一、什么是闭包

理论上讲,闭包和匿名函数是不同的概念,不过,php将其视作相同的概念。

闭包和匿名函数其实是伪装成函数的对象,如果审查php闭包和匿名函数,会发现他们是Closure类的实例,闭包和字符串或整数一样,也是一等值类型。

二、创建一个闭包

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

echo $closure('Yee Jason');

输出 Hello Yee Jason.

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

我通常把闭包当做函数和方法的回调使用,很多php函数都会用到回调函数,例如array_map和preg_replace_callback()是使用匿名函数的绝佳时机,记住,闭包和其他值一样,可以作为参数传入其他php函数。

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

print_r($numberPlusOne);

在PHP闭包之前, php开发者无法选择,只能单独创建具名函数,然后引用那个函数,这么做,代码执行的稍微慢一点, 而且把回调的实现和使用场所隔离开了,传统的php代码:

1
2
3
4
5
6
7
function incrementNumber($number)
{
return $number + 1;
}

$numberPlusOne = array_map('incrementNumber', [1, 2, 3]);
print_r($numberPlusOne);

以上两个例子输出:

1
Array ( [0] => 2 [1] => 3 [2] => 4 )

三、附加状态

前面演示了如何把匿名函数当成回调使用,下面探讨如何为php闭包附加并封装状态,javascript开发者可能对php的闭包感到奇怪,因为php闭包不会像真正的javascript闭包那样自动封装应用的状态,在php中,必须手动调用闭包对象的bindTo()方法或者使用use关键字,把状态附加到php闭包上。

使用use关键字附加闭包状态常见的多,因此我们先看这种方式,使用use关键字把变量附加到闭包上时,附加的变量会记住附件时付给他的值。

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

$clay = enclosePerson('Clay');

echo $clay('get me sweet tea!');

以上代码输出:Clay get me sweet tea

使用use关键字,把多个参数传入闭包时,需要还是用,号分隔开。

具名函数enclosePerson()有个名为$name的参数,这个函数返回一个闭包对象,而且这个闭包对象封装了 $name参数, 即便 返回的闭包对象跳出了enclosePerson()函数的作用域,它也会记住$name参数的值,因为$name变量仍在闭包中。

使用bindTo方法附加闭包的状态

别忘了php 闭包是对象,与任何其他的php对象类似,每个闭包实例都可以使用$this关键字获取闭包的内部状态。
闭包对象的默认状态没什么用,不过有一个 __invoke()魔术方法和bindTo()方法,仅此而已。
但是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
27
class APP
{
protected $routes = array();
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 dispath($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' . $this->responseBody);

echo $this->responseBody;
}
}

我们要特别注意addRoute方法,这个方法的参数分别是一个路由路径和路由回调,dispatch() 方法的参数是当前的HTTP请 求的 路径,它会调用匹配的路由回调,我们把路由绑定到当前的App实例上,这么做就能再回调函数中处理App实例的状态 。

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

匿名函数
提到闭包就不得不想起匿名函数,也叫闭包函数(closures),貌似PHP闭包实现主要就是靠它。声明一个匿名函数是这样:

1
2
3
$func = function() {

}; //带结束符

可以看到,匿名函数因为没有名字,如果要使用它,需要将其返回给一个变量。匿名函数也像普通函数一样可以声明参数,调用方法也相同:

1
2
3
4
5
6
$func = function( $param ) {
echo $param;
};
$func( 'some string' );
//输出:
//some string

顺便提一下,PHP在引入闭包之前,也有一个可以创建匿名函数的函数:create function,但是代码逻辑只能写成字符串,这样看起来很晦涩并且不好维护,所以很少有人用。

实现闭包
将匿名函数在普通函数中当做参数传入,也可以被返回。这就实现了一个简单的闭包。

下边有三个例子

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
//例一
//在函数里定义一个匿名函数,并且调用它
function printStr() {
$func = function( $str ) {
echo $str;
};
$func( 'some string' );
}
printStr();

//例二
//在函数中把匿名函数返回,并且调用它
function getPrintStrFunc() {
$func = function( $str ) {
echo $str;
};
return $func;
}
$printStrFunc = getPrintStrFunc();
$printStrFunc( 'some string' );

//例三
//把匿名函数当做参数传递,并且调用它
function callFunc( $func ) {
$func( 'some string' );
}
$printStrFunc = function( $str ) {
echo $str;
};
callFunc( $printStrFunc );
//也可以直接将匿名函数进行传递。如果你了解js,这种写法可能会很熟悉
callFunc( function( $str ) {
echo $str;
});

连接闭包和外界变量的关键字:USE

1
2
3
4
5
6
7
8
9
10
11
12
13
function getMoney() {
$rmb = 1;
$dollar = 6;
$func = function() use ( $rmb ) {
echo $rmb;
echo $dollar;
};
$func();
}
getMoney();
//输出:
//1
//报错,找不到dorllar变量

闭包可以保存所在代码块上下文的一些变量和值。PHP在默认情况下,匿名函数不能调用所在代码块的上下文变量,而需要通过使用use关键字。 换一个例子看看:

可以看到,dollar没有在use关键字中声明,在这个匿名函数里也就不能获取到它,所以开发中要注意这个问题。
有人可能会想到,是否可以在匿名函数中改变上下文的变量,但我发现是不可以的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function getMoney() {
$rmb = 1;
$func = function() use ( $rmb ) {
echo $rmb;
//把$rmb的值加1
$rmb++;
};
$func();
echo $rmb;
}
getMoney();
//输出:
//1
//1

啊,原来use所引用的也只不过是变量的一个副本而已。但是我想要完全引用变量,而不是复制。 要达到这种效果,其实在变量前加一个 & 符号就可以了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function getMoney() {
$rmb = 1;
$func = function() use ( &$rmb ) {
echo $rmb;
//把$rmb的值加1
$rmb++;
};
$func();
echo $rmb;
}
getMoney();
//输出:
//1
//2

好,这样匿名函数就可以引用上下文的变量了。如果将匿名函数返回给外界,匿名函数会保存use所引用的变量,而外界则不能得到这些变量,这样形成‘闭包’这个概念可能会更清晰一些。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function getMoneyFunc() {
$rmb = 1;
$func = function() use ( &$rmb ) {
echo $rmb;
//把$rmb的值加1
$rmb++;
};
return $func;
}$getMoney = getMoneyFunc();
$getMoney();
$getMoney();
$getMoney();
//输出:
//1
//2
//3

总结

PHP闭包的特性并没有太大惊喜,其实用CLASS就可以实现类似甚至强大得多的功能,更不能和js的闭包相提并论,只能期待PHP以后对闭包支持的改进。不过匿名函数还是挺有用的,比如在使用preg_replace_callback等之类的函数可以不用在外部声明回调函数了。

一、闭包总结
把一个闭包转换为某个类的方法(只是这个方法不需要通过对象调用), 这样闭包中的$this、static、self就转换成了对应的对象或类

把闭包当成对象的成员方法或者静态成员方法.

1
2
Closure::bind($cl1, null, 'A'); //就相当于在类里面加了个静态成员方法
Closure::bind($cl2, new A(), 'A'); //相当于在类里面加了个成员方法

成员方法中使用$this访问对象, 静态成员方法直接使用类名::成员的方法.
但是因为是匿名函数, 没有函数名, 所以返回一个已经绑定$this对象和类作用域的闭包给你使用.

二、闭包基本用法
闭包(Closure)又叫做匿名函数,也就是没有定义名字的函数。比如下面的例子:

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

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

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

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

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

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

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

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

1
2
3
4
Closure {   
public static Closure bind (Closure $closure , object $newthis [, mixed $newscope = 'static' ])
public Closure bindTo (object $newthis [, mixed $newscope = 'static' ])
}

方法说明:
Closure::bind: 复制一个闭包,绑定指定的 $this 对象和类作用域。
Closure::bindTo: 复制当前闭包对象,绑定指定的 $this 对象和类作用域。
下面将介绍
Closure::bind和Closure::bindTo
参数和返回值说明:
closure:表示需要绑定的闭包对象。
newthis:表示需要绑定到闭包对象的对象,或者 NULL 创建未绑定的闭包。
newscope:表示想要绑定给闭包的类作用域,可以传入类名或类的示例,默认值是’static’, 表示不改变。
该方法成功时返回一个新的 Closure 对象,失败时返回 FALSE。

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
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对象
$bindDog = Closure::bind($dog, new Animal(), 'Animal');// 给闭包绑定了Animal类的作用域,同时将Animal实例对象作为$this对象绑定给闭包
$bindPig = Closure::bind($pig, new Animal());// 将Animal实例对象作为$this对象绑定给闭包,保留闭包原有作用域
echo $bindCat(),'<br>';// 根据绑定规则,允许闭包通过作用域限定操作符获取Animal类静态私有成员属性
echo $bindDog(),'<br>';// 根据绑定规则,允许闭包通过绑定的$this对象(Animal实例对象)获取Animal实例私有成员属性
echo $bindPig(),'<br>';// 根据绑定规则,允许闭包通过绑定的$this对象获取Animal实例公有成员属性

// bindTo与bind类似,是面向对象的调用方式,这里只举一个,其他类比就可以
$bindCat = $cat->bindTo(null, 'Animal');

以上示例输出:

1
2
3
cat
dog
pig

四、连接闭包和外界变量的关键字:USE

闭包可以保存所在代码块上下文的一些变量和值。PHP在默认情况下,匿名函数不能调用所在代码块的上下文变量,而需要通过使用 use 关键字。

1
2
3
4
5
6
7
8
9
10
11
12
13
function getMoney() {
$rmb = 1;
$dollar = 6;
$func = function() use ( $rmb ) {
echo $rmb;
echo $dollar;
};
$func();
}
getMoney();
//输出:
//1
//报错,找不到dorllar变量

可以看到,dollar没有在use关键字中声明,在这个匿名函数里也就不能获取到它,所以开发中要注意这个问题。

有人可能会想到,是否可以在匿名函数中改变上下文的变量,但我发现是不可以的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function getMoney() {
$rmb = 1;
$func = function() use ( $rmb ) {
echo $rmb;
//把$rmb的值加1
$rmb++;
};
$func();
echo $rmb;
}
getMoney();
//输出:
//1
//1

原来use所引用的也只不过是变量的一个副本而已。但是我想要完全引用变量,而不是复制。要达到这种效果,其实在变量前加一个 & 符号就可以了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function getMoneyFunc() {
$rmb = 1;
$func = function() use ( &$rmb ) {
echo $rmb;
//把$rmb的值加1
$rmb++;
};
return $func;
}
$getMoney = getMoneyFunc();
$getMoney();
$getMoney();
$getMoney();
//输出:
//1
//2
//3

匿名函数
实现一个简单的匿名函数:

上面就是一个简单的匿名函数,定义一个函数体,将函数体赋值给一个变量(php5.3之后支持该写法)。

实现闭包
1、当做参数传递:

2、将匿名函数返回:

捕获外界变量
闭包: 闭包是词法作用于的体现,一个持有外部环境自由变量的函数就是闭包。闭包体现的是在程序运行过程中,由 “不确定”变为“ 确定” 的过程。

捕获外部变量:在PHP中对捕获这一动作有了更清晰的表现,使用use关键字。如上面例2。

在上面的例2中,匿名函数$func通过use关键字捕获了外部的自由变量$param,在调用时通过传入cFunc()函数的参数123($param此时会变为“确定”状态),进而调用匿名函数时输出“params:456 123”。

use引入的是自由变量的副本。

golang闭包: 在golang中同样通过匿名函数实现了闭包,和PHP不同的是,golang中的闭包是默认会引入上下文的自由变量,且引入的地址,即在闭包函数内部修改变量会在函数外部生效。

PHP Closure类
用于代表匿名函数类。在PHP中定义一个闭包函数其实就是一个Closure类的实例。

类摘要

1
2
3
4
5
6
7
8
9
Closure {

/* 方法 */

__construct ( void )

public static bind ( Closure $closure , object $newthis [, mixed $newscope = 'static' ] ) : Closure

public bindTo ( object $newthis [, mixed $newscope = 'static' ] ) : Closure

Closure::__construct — 用于禁止实例化的构造函数

Closure::bind — 复制一个闭包,绑定指定的$this对象和类作用域。
Closure::bindTo — 复制当前闭包对象,绑定指定的$this对象和类作用域。
Closure::bind
复制一个闭包,绑定指定的$this对象和类作用域,返回一个新的匿名函数

参数说明:
closure: 需要绑定的匿名函数。
newthis: 需要绑定到匿名函数的对象,或者 NULL 创建未绑定的闭包。( 理解:可以选择是否将匿名函数绑定到一个类对象,若绑定到了一个类对象,则可以在匿名函数内使用 $this ,否则不可使用。 )
newscope: 想要绑定给闭包的类作用域,或者 ‘static’ 表示不改变。如果传入一个对象,则使用这个对象的类型名。类作用域用来决定在闭包中 $this 对象的 私有、保护方法 的可见性。( 理解:如果传入一个类,则可以访问类的static、private、protected属性,否则只能访问public属性。 )
简单理解:可以简单理解为将该匿名函数绑定到一个类或实例。根据参数的不同,可以访问不同的类的属性。

输出:

1
2
3
4
5
6
7
8
9
10
11
bind cat

string(3) "cat"

bind dog

string(3) "dog"

bind dog2

Fatal error: Using $this when not in object context

Closure::bindTo
Closure::bind()的非静态形式。

小结

PHP通过匿名函数实现闭包。
可以通过将匿名函数作为参数或返回值实现闭包。
可以通过use关键字引入外部变量,且引入的变量副本。
匿名函数均实现了Closure类,且可以通过Closure::bind()方法将匿名函数绑定到某个类。以上是文章全部内容,有需要学习交流的友人请加入Swoole交流群的咱们一起,有问题一起交流,一起进步!前提是你是学技术的。感谢阅读!

面向对象变成语言代码的复用主要采用继承来实现,而函数的复用,就是通过闭包来实现。这就是闭包的设计初衷。

注:PHP里面闭包函数是为了复用函数而设计的语言特性,如果在闭包函数里面访问指定域的变量,使用use关键字来实现。

PHP具有面向函数的编程特性,但是也是面向对象编程语言,PHP 会自动把闭包函数转换成内置类 Closure 的对象实例,依赖Closure 的对象实例又给闭包函数添加了更多的能力。

闭包不能被实例(私有构造函数),也不能被继承(finally 类)。可以通过反射来判断闭包实例是否能被实例,继承。

匿名函数

提到闭包就不得不想起匿名函数,也叫闭包函数(closures),貌似PHP闭包实现主要就是靠它。声明一个匿名函数是这样:

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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
$func = function() {

}; //带结束符
```  

可以看到,匿名函数因为没有名字,如果要使用它,需要将其返回给一个变量。匿名函数也像普通函数一样可以声明参数,调用方法也相同:

```php
$func = function( $param ) {
echo $param;
};
$func( 'some string' );

//输出:
//some string
```  

顺便提一下,PHP在引入闭包之前,也有一个可以创建匿名函数的函数:create function,但是代码逻辑只能写成字符串,这样看起来很晦涩并且不好维护,所以很少有人用。



实现闭包

将匿名函数在普通函数中当做参数传入,也可以被返回。这就实现了一个简单的闭包。


连接闭包和外界变量的关键字:USE


PHP在默认情况下,匿名函数不能调用所在代码块的上下文变量,而需要通过使用use关键字。

```php
function getMoney() {
$rmb = 1;
$func = function() use ( $rmb ) {
echo $rmb;
//把$rmb的值加1
$rmb++;
};
$func();
echo $rmb; //闭包内的变量改变了,但是闭包外没有改变。
}
getMoney();

//输出:
//1
//1
```  

注:use所引用的是变量的复制(副本而),并不是完全引用变量。如果要达到引用的效果,就需要使用 & 符号,进行引用传递参数。

```php
function getMoney() {
$rmb = 1;
$func = function() use ( &$rmb ) {
echo $rmb;
//把$rmb的值加1
$rmb++;
};
$func();
echo $rmb;
}
getMoney();

//输出:
//1
//2
```  




总结:


闭包函数不能直接访问闭包外的变量,而是通过use 关键字来调用上下文变量(闭包外的变量),也就是说通过use来引用上下文的变量;

闭包内所引用的变量不能被外部所访问(即,内部对变量的修改,外部不受影响),若想要在闭包内对变量的改变从而影响到上下文变量的值,需要使用&的引用传参。



PHP Closure 类是用于代表匿名函数的类,匿名函数(在 PHP 5.3 中被引入)会产生这个类型的对象,Closure类摘要如下:
```php
Closure {
__construct ( void )
public static Closure bind (Closure $closure , object $newthis [, mixed $newscope = 'static' ])
public Closure bindTo (object $newthis [, mixed $newscope = 'static' ])
}
```  

方法说明:


Closure::\_\_construct — 用于禁止实例化的构造函数
Closure::bind — 复制一个闭包,绑定指定的$this对象和类作用域。
Closure::bindTo — 复制当前闭包对象,绑定指定的$this对象和类作用域。
  

除了此处列出的方法,还有一个 \_\_invoke 方法。这是为了与其他实现了 \_\_invoke()魔术方法 的对象保持一致性,但调用闭包对象的过程与它无关。



参数说明:

closure表示需要绑定的闭包对象。
newthis表示需要绑定到闭包对象的对象,或者NULL创建未绑定的闭包。
newscope表示想要绑定给闭包的类作用域,可以传入类名或类的示例,默认值是 'static', 表示不改变。



返回值:成功时返回一个新的 Closure 对象,失败时返回FALSE



Closure::bindClosure::bindTo的静态版本



例子:
```php
class Animal {
public $cat = 'cat';
public static $dog = 'dog';
private $pig = 'pig';
private static $duck = 'duck';
}

//不能通过 $this 访问静态变量
//不同通过 类名::私有静态变量,只能通过self,或者static,在类里面访问私有静态变量

$cat = function() {
return $this->cat;
};

$dog = static function () {
return Animal::$dog;
};

$pig = function() {
return $this->pig;
};

$duck = static function() {
//return Animal::$duck; 这样写,会报错,提示不能通过类名访问私有静态变量
return self::$duck; // return static::$duck
};

$bindCat = Closure::bind($cat, new Animal(), 'Animal');
$bindCat2 = Closure::bind($cat, new Animal(), new Animal());
echo $bindCat() . PHP_EOL;
echo $bindCat2() . PHP_EOL;

$bindDog = Closure::bind($dog, null, 'Animal');
$bindDog2 = Closure::bind($dog, null, new Animal());
echo $bindDog() . PHP_EOL;
echo $bindDog2() . PHP_EOL;

$bindPig = Closure::bind($pig, new Animal(), 'Animal');
$bindPig2 = Closure::bind($pig, new Animal(), new Animal());
echo $bindPig() . PHP_EOL;
echo $bindPig2() . PHP_EOL;

$bindDuck = Closure::bind($duck, null, 'Animal');
$bindDuck2 = Closure::bind($duck, null, new Animal());
echo $bindDuck() . PHP_EOL;
echo $bindDuck2() . PHP_EOL;
```

通过上面的例子,可以看出函数复用得,可以把函数挂在不同的类上,或者对象上。

 

总结:

1. 闭包内如果用 $this, 则 $this 只能调用非静态的属性,这和实际类中调用原则是一致的,且 Closure::bind() 方法的第2个参数不能为null,必须是一个实例 (因为$this,必须在实例中使用),第三个参数可以是实例,可以是类字符串,或 static

2. 闭包内调用静态属性时,闭包必须声明为 static,同时Closure::bind()方法的第2个参数需要为null,因为 静态属性不需要实例,第3个参数可以是类字符串,实例,staic.

php的闭包(Closure)也就是匿名函数。是PHP5.3引入的。
闭包的语法很简单,需要注意的关键字就只有useuse意思是连接闭包和外界变量。

```php
$a = function() use($b) {

}

闭包的几个作用:
1 减少foreach的循环的代码
比如手册http://php.net/manual/en/functions.anonymous.php 中的例子Cart

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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
<?php
// 一个基本的购物车,包括一些已经添加的商品和每种商品的数量。
// 其中有一个方法用来计算购物车中所有商品的总价格。该方法使用了一个closure作为回调函数。
class Cart
{
const PRICE_BUTTER = 1.00;
const PRICE_MILK = 3.00;
const PRICE_EGGS = 6.95;

protected $products = array();

public function add($product, $quantity)
{
$this->products[$product] = $quantity;
}

public function getQuantity($product)
{
return isset($this->products[$product]) ? $this->products[$product] :
FALSE;
}

public function getTotal($tax)
{
$total = 0.00;

$callback =
function ($quantity, $product) use ($tax, &$total)
{
$pricePerItem = constant(__CLASS__ . "::PRICE_" .
strtoupper($product));
$total += ($pricePerItem * $quantity) * ($tax + 1.0);
};

array_walk($this->products, $callback);
return round($total, 2);;
}
}

$my_cart = new Cart;

// 往购物车里添加条目
$my_cart->add('butter', 1);
$my_cart->add('milk', 3);
$my_cart->add('eggs', 6);

// 打出出总价格,其中有 5% 的销售税.
print $my_cart->getTotal(0.05) . "\n";
// The result is 54.29
?>
```

这里如果我们改造getTotal函数必然要使用到foreach

2 减少函数的参数
```php
function html ($code , $id="", $class=""){

if ($id !== "") $id = " id = \"$id\"" ;

$class = ($class !== "")? " class =\"$class\"":">";

$open = "<$code$id$class";

$close = "</$code>";

return function ($inner = "") use ($open, $close){

return "$open$inner$close";};

}

如果是使用平时的方法,我们会把inner放到html函数参数中,这样不管是代码阅读还是使用都不如使用闭包

3 解除递归函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php
$fib = function($n) use(&$fib) {
if($n == 0 || $n == 1) return 1;
return $fib($n - 1) + $fib($n - 2);
};

echo $fib(2) . "\n"; // 2
$lie = $fib;
$fib = function(){die('error');};//rewrite $fib variable
echo $lie(5); // error because $fib is referenced by closure
```

注意上题中的use使用了&,这里不使用&会出现错误fib(n-1)是找不到function的(前面没有定义fib的类型)

所以想使用闭包解除循环函数的时候就需要使用

```php
<?php
$recursive = function () use (&$recursive){
// The function is now available as $recursive
}

这样的形式

4 关于延迟绑定
如果你需要延迟绑定use里面的变量,你就需要使用引用,否则在定义的时候就会做一份拷贝放到use中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
$result = 0;

$one = function()
{ var_dump($result); };

$two = function() use ($result)
{ var_dump($result); };

$three = function() use (&$result)
{ var_dump($result); };

$result++;

$one(); // outputs NULL: $result is not in scope
$two(); // outputs int(0): $result was copied
$three(); // outputs int(1)

使用引用和不使用引用就代表了是调用时赋值,还是申明时候赋值

PHP Closure类是用于代表匿名函数的类,匿名函数(在PHP5.3中被引入)会产生这个类型的对象,Closure类摘要如下:

1
2
3
4
5
Closure {
__construct ( void )
public static Closure bind (Closure $closure , object $newthis [, mixed $newscope = 'static' ])
public Closure bindTo (object $newthis [, mixed $newscope = 'static' ])
}

方法说明:

  • Closure::__construct — 用于禁止实例化的构造函数
  • Closure::bind — 复制一个闭包,绑定指定的$this对象和类作用域。
  • Closure::bindTo — 复制当前闭包对象,绑定指定的$this对象和类作用域。

除了此处列出的方法,还有一个__invoke 方法。这是为了与其他实现了__invoke()魔术方法 的对象保持一致性,但调用闭包对象的过程与它无关。

下面将介绍Closure::bind和Closure::bindTo。

Closure::bind是Closure::bindTo的静态版本,其说明如下:

1
public static Closure bind (Closure $closure , object $newthis [, mixed $newscope = 'static' ])

  • closure表示需要绑定的闭包对象。
  • newthis表示需要绑定到闭包对象的对象,或者NULL创建未绑定的闭包。
  • newscope表示想要绑定给闭包的类作用域,可以传入类名或类的示例,默认值是 ‘static’, 表示不改变。

该方法成功时返回一个新的Closure对象,失败时返回FALSE。

例子说明:

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
<?php
/**
* 复制一个闭包,绑定指定的$this对象和类作用域。
*
* @author 疯狂老司机
*/
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对象
$bindDog = Closure::bind($dog, new Animal(), 'Animal');// 给闭包绑定了Animal类的作用域,同时将Animal实例对象作为$this对象绑定给闭包
$bindPig = Closure::bind($pig, new Animal());// 将Animal实例对象作为$this对象绑定给闭包,保留闭包原有作用域
echo $bindCat(),'<br>';// 根据绑定规则,允许闭包通过作用域限定操作符获取Animal类静态私有成员属性
echo $bindDog(),'<br>';// 根据绑定规则,允许闭包通过绑定的$this对象(Animal实例对象)获取Animal实例私有成员属性
echo $bindPig(),'<br>';// 根据绑定规则,允许闭包通过绑定的$this对象获取Animal实例公有成员属性
?>

输出:

1
2
3
cat
dog
pig

Closure::bindTo — 复制当前闭包对象,绑定指定的$this对象和类作用域,其说明如下:

1
public Closure Closure::bindTo (object $newthis [, mixed $newscope = 'static' ])

newthis表示绑定给闭包对象的一个对象,或者NULL来取消绑定。
newscope表示关联到闭包对象的类作用域,可以传入类名或类的示例,默认值是 ‘static’, 表示不改变。
该方法创建并返回一个闭包对象,它与当前对象绑定了同样变量,但可以绑定不同的对象,也可以绑定新的类作用域。绑定的对象决定了返回的闭包对象中的$this的取值,类作用域决定返回的闭包对象能够调用哪些方法,也就是说,此时$this可以调用的方法,与newscope类作用域相同。

例子1:

1
2
3
4
5
6
7
8
<?php
function __autoload($class) {
require_once "$class.php";
}

$template = new Template;
$template->render(new Article, 'tpl.php');
?>

Template.php 模板类

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
<?php
/**
* 模板类,用于渲染输出
*
* @author 疯狂老司机
*/
class Template{
/**
* 渲染方法
*
* @access public
* @param obj 信息类
* @param string 模板文件名
*/
public function render($context, $tpl){
$closure = function($tpl){
ob_start();
include $tpl;
return ob_end_flush();
};
$closure = $closure->bindTo($context, $context);
$closure($tpl);
}

}

Article.php 信息类

1
2
3
4
5
6
7
8
9
10
<?php
/**
* 文章信息类
*
* @author 疯狂老司机
*/
class Article{
private $title = "这是文章标题";
private $content = "这是文章内容";
}

tpl.php 模板文件

1
2
3
4
5
6
7
8
9
10
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
</head>
<body>
<h1><?php echo $this->title;?></h1>
<p><?php echo $this->content;?></p>
</body>
</html>

运行时确保以上文件位于同级目录。
输出:

1
2
这是文章标题
这是文章内容

例子2:

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
45
46
47
48
<?php
/**
* 给类动态添加新方法
*
* @author 疯狂老司机
*/
trait DynamicTrait {

/**
* 自动调用类中存在的方法
*/
public function __call($name, $args) {
if(is_callable($this->$name)){
return call_user_func($this->$name, $args);
}else{
throw new \RuntimeException("Method {$name} does not exist");
}
}
/**
* 添加方法
*/
public function __set($name, $value) {
$this->$name = is_callable($value)?
$value->bindTo($this, $this):
$value;
}
}

/**
* 只带属性不带方法动物类
*
* @author 疯狂老司机
*/
class Animal {
use DynamicTrait;
private $dog = 'dog';
}

$animal = new Animal;

// 往动物类实例中添加一个方法获取实例的私有属性$dog
$animal->getdog = function() {
return $this->dog;
};

echo $animal->getdog();

?>

输出:

1
dog

例子3:

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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
<?php
/**
* 一个基本的购物车,包括一些已经添加的商品和每种商品的数量
*
* @author 疯狂老司机
*/
class Cart {
// 定义商品价格
const PRICE_BUTTER = 1.00;
const PRICE_MILK = 3.33;
const PRICE_EGGS = 8.88;

protected $products = array();

/**
* 添加商品和数量
*
* @access public
* @param string 商品名称
* @param string 商品数量
*/
public function add($item, $quantity) {
$this->products[$item] = $quantity;
}

/**
* 获取单项商品数量
*
* @access public
* @param string 商品名称
*/
public function getQuantity($item) {
return isset($this->products[$item]) ? $this->products[$item] : FALSE;
}

/**
* 获取总价
*
* @access public
* @param string 税率
*/
public function getTotal($tax) {
$total = 0.00;

$callback = function ($quantity, $item) use ($tax, &$total) {
$pricePerItem = constant(__CLASS__ . "::PRICE_" . strtoupper($item));
$total += ($pricePerItem * $quantity) * ($tax + 1.0);
};

array_walk($this->products, $callback);
return round($total, 2);;
}
}

$my_cart = new Cart;

// 往购物车里添加商品及对应数量
$my_cart->add('butter', 10);
$my_cart->add('milk', 3);
$my_cart->add('eggs', 12);

// 打出出总价格,其中有 5% 的销售税.
echo $my_cart->getTotal(0.05);

?>

输出:

1
132.88

补充说明:闭包可以使用USE关键连接外部变量。

总结:合理使用闭包能使代码更加简洁和精炼。

PHP核心特性-匿名函数

提出

在匿名函数出现之前,所有的函数都需要先命名才能使用:

1
2
3
4
5
6
function increment($value)
{
return $value + 1;
}

array_map('increment', [1, 2, 3]);

有的时候函数可能只需要使用一次,这时候使用匿名函数会使得代码更加简洁直观,同时也避免了函数在其他地方被使用:

1
2
3
array_map(function($value){
return $value + 1;
}, [1, 2, 3]);

定义和使用

PHP将闭包和匿名函数视为同等概念(本文统称为匿名函数),本质上都是伪装成函数的对象。

匿名函数的本质是对象,因此跟对象一样可将匿名函数赋值给某一变量:

1
2
3
4
5
$greet = function(string $name){
echo "hello {$name}";
}

$greet("jack") // hello jack

所有的匿名函数都是Closure对象的实例:

1
$greet instanceof Closure // true

对象并没有什么父作用域可言,所以需要使用use来手动声明使用的变量:

1
2
3
4
5
6
7
$num = 1;
$func = function() use($num){
$num = $num + 1;
echo $num;
}
$func(); // 2
echo $num; // 还是 1

如果要让匿名函数中的变量生效,需要使用引用传值:

1
2
3
4
5
6
7
$num = 1;
$func = function() use(&$num){
$num = $num + 1;
echo $num;
}
$func(); // 2
echo $num; // 2

从PHP5.4开始,在类里面使用匿名函数时,匿名函数的$this将自动绑定到当前类:

1
2
3
4
5
6
7
8
9
10
11
12
class Foo {
public function bar()
{
return function() {
return $this;
};
}
}

$foo = new Foo();
$obj = $foo->bar(); // Closure()
$obj(); // Foo

如果不想让自动绑定生效,可使用静态匿名函数:

1
2
3
4
5
6
7
8
9
10
11
class Foo {
public function bar()
{
return static function() {
return $this;
};
}
}
$foo = new Foo();
$obj = $foo->bar(); // Closure()
$obj(); // Using $this when not in object context

匿名函数的本质

匿名函数的本质是Closure对象,包括了以下五个方法

1
2
3
4
5
6
7
Closure {
private __construct ( void )
public static bind ( Closure $closure , object $newthis [, mixed $newscope = "static" ] ) : Closure
public bindTo ( object $newthis [, mixed $newscope = "static" ] ) : Closure
public call ( object $newthis [, mixed $... ] ) : mixed
public static fromCallable ( callable $callable ) : Closure
}

__construct - 防止匿名函数被实例化

1
2
$closure = new \Closure();
// PHP Error: Instantiation of 'Closure' is not allowed

Closure::bindTo - 复制当前匿名函数对象,绑定指定的 $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
// 定义商品类
class Good {
private $price;

public function __construct(float $price)
{
$this->price = $price;
}
}

// 定义一个匿名函数,计算商品的促销价
$addDiscount = function(float $discount = 0.8){
return $this->price * $discount;
}

$good = new Good(100);

// 将匿名函数绑定到 $good 实例,同时指定作用域为 Good
$count = $addDiscount->bindTo($good, Good::class);
$count(); // 80

// 将匿名函数绑定到 $good 实例,但是不指定作用域,将无法访问 $good 的私有属性
$count = $addDiscount->bindTo($good);
$count(); // 报错

Closure::bind - bindTo 方法的静态版本,有两种用法:

用法一:实现与bindTo方法同样的效果

1
$count = \Closure::bind($addDiscount, $good, Good::class);

用法二:将匿名函数与类(而不是对象)绑定,记得要将第二个参数设置为 null

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 商品库存为 10
class Good {
static $num = 10;
}

// 每次销售后返回当前库存
$sell = static function() {
return"当前库存为". --static::$num ;
};

// 将静态匿名函数绑定到 Good 类中
$sold = \Closure::bind($sell, null, Good::class);

$sold(); // 当前库存为 9
$sold(); // 当前库存为 8

call - PHP7新增的call方法可以实现绑定并调用匿名函数,除了语法更加简洁外,性能也更高

1
2
3
4
5
6
// call 版本
$addDiscount->call($good, 0.5); // 绑定并传入参数 0.5,结果为 50

// bindTo 版本
$count = $addDiscount->bindTo($good, Good::class);
$count(0.5); // 50

fromCallable - 将给定的 callable 函数转化成匿名函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Good {
private $price;

public function __construct(float $price)
{
$this->price = $price;
}
}

function addDiscount(float $discount = 0.8){
return $this->price * $discount;
}

$closure = \Closure::fromCallable('addDiscount');
$good = new Good(100);
$count = $closure->bindTo($good);
$count = $closure->bindTo($good, Good::class); // 报错,不能重复绑定作用域
$count(); // 报错,无法访问私有属性

fromCallable 等价于

1
2
$reflexion = new ReflectionFunction('addDiscount');
$closure = $reflexion->getClosure();

这里有一点需要特别注意的是,无论是fromCallable转化成的闭包,还是使用反射得到的闭包,在使用 bindTo 时,如果第二个参数指定绑定类,会报错

1
Cannot rebind scope of closure created by ReflectionFunctionAbstract::getClosure()

0%