0x00 前言
閉包是指在建立時封裝周圍狀態的函式。即使閉包所在的環境不存在了,閉包中封裝的狀態依然存在。
在 PHP 裡所有的閉包都是 Clourse
類所例項化的一個物件,也就是說閉包與其他 PHP 物件沒有什麼不同。而一個物件就必然有其方法和屬性,這篇文章將總結 PHP 中閉包的基礎用法和 Clourse
類方法的作用。
0x01 閉包基本用法
下面看看最基本的閉包使用方法:
<?php
$hello = function ($word) {
return `hello ` . $word;
};
echo $hello(`world`);
// 輸出 hello world
複製程式碼
嘿,這段程式碼最直觀的感受就是將一個函式賦值給了 $hello
變數,然後通過 $hello
直接呼叫它。但是這個閉包並沒有從父作用域中繼承變數(就是封裝周圍狀態),我們可以通過 use
關鍵字從閉包的父作用域繼承變數。示例如下:
<?php
$name = `panda`;
$hello = function () use ($name) {
return `hello ` . $name;
};
echo $hello();
// 輸出 hello panda
複製程式碼
PHP 7.1 起,
use
不能傳入此類變數: superglobals、 $this 或者和引數重名。
此外在使用 use
關鍵字時,父作用域的變數是通過值傳遞進閉包的。也就是說一旦閉包建立完成,外部的變數即使修改也不會影響傳遞進閉包內的值(就是即使閉包所在的環境不存在了,閉包中封裝的狀態依然存在)。示例如下:
<?php
$name = `panda`;
$hello = function () use ($name) {
return `hello ` . $name;
};
$name = `cat`;
echo $hello();
// 輸出 hello panda
複製程式碼
傳遞變數的引用可以使閉包修改外部變數的值,示例如下:
<?php
$name = `panda`;
$changeName = function () use (&$name) {
$name = `cat`;
};
$changeName();
echo $name;
// 輸出 cat
複製程式碼
注意:PHP 中傳遞物件時,預設是以引用傳遞所以在閉包內操作 use
傳遞的物件時需要特別注意。示例如下:
<?php
class Dog {
public $name = `Wang Cai`;
}
$dog = new Dog();
$changeName = function () use ($dog) {
$dog->name = `Lai Fu`;
};
$changeName();
echo $dog->name;
// 輸出 Lai Fu
複製程式碼
0x02 Clourse 類
證明閉包只是 Clourse 類物件
<?php
$clourse = function () {
echo `hello clourse`;
};
if (is_object($clourse)) {
echo get_class($clourse);
}
// 輸出 Closure
複製程式碼
上面的程式碼將輸出 Closure 證明了閉包只是一個普通的 Closure
類物件。
Clourse 類摘要
我們可以從 PHP 官方手冊 看到閉包類的相關資訊,下面是我在 PhpStorm 的本地文件檢視到 Clourse
類摘要。
/**
* Class used to represent anonymous functions.
* <p>Anonymous functions, implemented in PHP 5.3, yield objects of this type.
* This fact used to be considered an implementation detail, but it can now be relied upon.
* Starting with PHP 5.4, this class has methods that allow further control of the anonymous function after it has been created.
* <p>Besides the methods listed here, this class also has an __invoke method.
* This is for consistency with other classes that implement calling magic, as this method is not used for calling the function.
* @link http://www.php.net/manual/en/class.closure.php
*/
final class Closure {
/**
* This method exists only to disallow instantiation of the Closure class.
* Objects of this class are created in the fashion described on the anonymous functions page.
* @link http://www.php.net/manual/en/closure.construct.php
*/
private function __construct() { }
/**
* This is for consistency with other classes that implement calling magic,
* as this method is not used for calling the function.
* @param mixed $_ [optional]
* @return mixed
* @link http://www.php.net/manual/en/class.closure.php
*/
public function __invoke(...$_) { }
/**
* Duplicates the closure with a new bound object and class scope
* @link http://www.php.net/manual/en/closure.bindto.php
* @param object $newthis The object to which the given anonymous function should be bound, or NULL for the closure to be unbound.
* @param mixed $newscope The class scope to which associate the closure is to be associated, or `static` to keep the current one.
* If an object is given, the type of the object will be used instead.
* This determines the visibility of protected and private methods of the bound object.
* @return Closure Returns the newly created Closure object or FALSE on failure
*/
function bindTo($newthis, $newscope = `static`) { }
/**
* This method is a static version of Closure::bindTo().
* See the documentation of that method for more information.
* @static
* @link http://www.php.net/manual/en/closure.bind.php
* @param Closure $closure The anonymous functions to bind.
* @param object $newthis The object to which the given anonymous function should be bound, or NULL for the closure to be unbound.
* @param mixed $newscope The class scope to which associate the closure is to be associated, or `static` to keep the current one.
* If an object is given, the type of the object will be used instead.
* This determines the visibility of protected and private methods of the bound object.
* @return Closure Returns the newly created Closure object or FALSE on failure
*/
static function bind(Closure $closure, $newthis, $newscope = `static`) { }
/**
* Temporarily binds the closure to newthis, and calls it with any given parameters.
* @link http://php.net/manual/en/closure.call.php
* @param object $newThis The object to bind the closure to for the duration of the call.
* @param mixed $parameters [optional] Zero or more parameters, which will be given as parameters to the closure.
* @return mixed
* @since 7.0
*/
function call ($newThis, ...$parameters) {}
/**
* @param callable $callable
* @return Closure
* @since 7.1
*/
public static function fromCallable (callable $callable) {}
}
複製程式碼
首先 Clourse
類為 final
類,也就是說它將無法被繼承,其次它的建構函式 __construct
被設為 private
即無法通過 new
關鍵字例項化閉包物件,這兩點保證了閉包只能通過 function (...) use(...) {...}
這種語法例項化 。
為什麼閉包可以當作函式執行?
從上面的類摘要中我們看出 Clourse
類實現了 __invoke 方法,在 PHP 官方手冊中對該方法解釋如下:
當嘗試以呼叫函式的方式呼叫一個物件時,__invoke() 方法會被自動呼叫。
這就是閉包可以被當作函式執行的原因。
繫結指定的$this物件和類作用域
在允許使用閉包路由的框架中(如:Slim),我們可以看見如下寫法:
$app->get(`/test`, function () {
echo $this->request->getMethod();
});
複製程式碼
在一個閉包居然能中使用 $this
?這個 $this
指向哪個物件?
通過 bindTo
和 bind
方法都能夠實現繫結 $this
和類作用域的功能,示例如下:
<?php
class Pandas {
public $num = 1;
}
$pandas = new Pandas();
$add = function () {
echo ++$this->num . PHP_EOL;
};
$newAdd1 = $add->bindTo($pandas);
$newAdd1();
// 輸出 2
$newAdd2 = Closure::bind($add, $pandas);
$newAdd2();
// 輸出 3
複製程式碼
上面的這段例子將指定物件繫結為閉包的 $this
,但是我們並沒有指定類作用域。所以如果將 Pandas
類的 $num
屬性改寫為 protected
或 private
則會丟擲一個致命錯誤!
Fatal error: Uncaught Error: Cannot access protected property Pandas::$num
在需要訪問繫結物件的非公開屬性或方法時,我們需要指定類作用域,示例如下:
<?php
class Pandas {
protected $num = 1;
}
$pandas = new Pandas();
$add = function () {
echo ++$this->num . PHP_EOL;
};
$newAdd1 = $add->bindTo($pandas, $pandas);
$newAdd1();
// 輸出 2
$newAdd2 = Closure::bind($add, $pandas, `Pandas`);
$newAdd2();
// 輸出 3
複製程式碼
這裡我們看見 bindTo
和 bind
方法都指定了 $newscope
引數,$newscope
引數預設為 static
即不改變類作用域。$newscope
引數接受類名或物件,並將閉包的類作用域改為指定的類作用域,此時 Pandas
類的 $num
屬性便能夠被閉包訪問。
一次性繫結 $this 物件和類作用域並執行(PHP7)
bindTo
和 bind
方法每次指定新的物件和類作用域時都要將原閉包進行復制然後返回新的閉包,在需要多次修改繫結物件的情景下便顯得繁瑣,所以 PHP7 提供了一個新的方法 call
它能將閉包臨時的繫結到一個物件中(類作用域同時被修改為該物件所屬的類)並執行。示例如下:
<?php
class Pandas {
protected $num = 1;
}
$pandas = new Pandas();
$add = function ($num) {
$this->num += $num;
echo $this->num . PHP_EOL;
};
$add->call($pandas, 5);
// 輸出 6
複製程式碼
Callable 轉為閉包(PHP7.1)
在 PHP7.1 中 Closure
類存在 fromCallable
方法能夠將 callable
型別的值轉為閉包,示例如下:
<?php
class Foo
{
protected $num = 1;
public static function hello(string $bar)
{
echo `hello ` . $bar;
}
}
$hello = Closure::fromCallable([`Foo`, `hello`]);
$hello(`world`);
複製程式碼
這種寫法還是挺爽的畢竟通過閉包呼叫總比用 call_user_func
函式呼叫爽的多^_^。
0x03 總結
更多相關內容請看 Closure 類 和 匿名函式,因為 PHP 官方手冊中文版的 Closure 類沒有更新,所以沒有 call
和 fromCallable
方法的內容,推薦大家看英文版(ㄒoㄒ)。