PHP 閉包那些事兒

楓戀寒發表於2019-02-16

匿名函式

匿名函式,也叫閉包函式,說白了就是“沒有名字的函式”,和一般函式結構一樣,只是少了函式名以及最後需要加上分號;

注:理論上講閉包和匿名函式是不同的概念,不過PHP將其視作相同的概念。

$func = function()
{
    echo `Hello World` . PHP_EOL;
};
$func();

匿名函式和普通函式的區分有:

  • 匿名函式也可以作為變數的值來使用。
  • 匿名函式可以從父作用域繼承變數,而這個父作用域是定義該閉包的函式(不一定是呼叫它的函式)。
$message = `hello`;
$example = function () use ($message) {
    return $message;
};
$message = `world`;
echo $example();

輸出:hello

注意:必須使用use關鍵字將變數傳遞進去才行,具體見官方文件

閉包類

定義一個閉包函式,其實就是例項化一個閉包類(Closure)物件:

$func = function()
{
    echo `hello world` . PHP_EOL;
};
var_dump($func);

輸出:
object(Closure)#1 (0) {
}

類摘要:

Closure {
     __construct ( void )
     public static Closure bind ( Closure $closure , object $newthis [, mixed $newscope = `static` ] )
     public Closure bindTo ( object $newthis [, mixed $newscope = `static` ] )
}

除了以上方法,閉包還實現了一個__invoke()魔術方法,當嘗試以呼叫函式的方式呼叫一個物件時,__invoke() 方法會被自動呼叫。

bindTo 方法

接下來我們來看看bindTo方法,通過該方法,我們可以把閉包的內部狀態繫結到其他物件上。這裡bindTo方法的第二個引數顯得尤為重要,其作用是指定繫結閉包的那個物件所屬的PHP類,這樣,閉包就可以在其他地方訪問繫結閉包的物件中受保護和私有的成員變數。

你會發現,PHP框架經常使用bindTo方法把路由URL對映到匿名回撥函式上,框架會把匿名回撥函式繫結到應用物件上,這樣在匿名函式中就可以使用$this關鍵字引用重要的應用物件:

class App {
    protected $routes = [];
    protected $responseStatus = `200 OK`;
    protected $responseContentType = `text/html`;
    protected $responseBody = `Hello World`;

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

    public function dispatch($path) {
        foreach ($this->routes as $routePath => $callback) {
            if( $routePath === $path) {
                $callback();
            }
        }
        header(`HTTP/1.1 ` . $this->responseStatus);
        header(`Content-Type: ` . $this->responseContentType);
        header(`Content-Length: ` . mb_strlen($this->responseBody));
        echo $this->responseBody;
    }

}

這裡我們需要重點關注addRoute方法,這個方法的引數分別是一個路由路徑和一個路由回撥,dispatch方法的引數是當前HTTP請求的路徑,它會呼叫匹配的路由回撥。第9行是重點所在,我們將路由回撥繫結到了當前的App例項上。這麼做能夠在回撥函式中處理App例項的狀態:

$app = new App();
$app->addRoute(‘/user’, function(){
    $this->responseContentType = ‘application/json;charset=utf8’;
    $this->responseBody = `世界你好`;
});
$app->dispatch(`/user`);

IoC 容器

匿名函式可以從父作用域繼承變數,而這個父作用域是定義該閉包的函式(不一定是呼叫它的函式)。

利用這個特性,我們可以實現一個簡單的控制反轉IoC容器:

class Container
{
    protected static $bindings;
 
    public static function bind($abstract, Closure $concrete)
    {
        static::$bindings[$abstract] = $concrete;
    }
 
    public static function make($abstract)
    {
        return call_user_func(static::$bindings[$abstract]);
    }
}
 
class talk
{
    public function greet($target)
    {
        echo `Hello ` . $target->getName();
    }
}

class A
{
    public function getName()
    {
        return `World`;
    }
}
 
// 建立一個talk類的例項
$talk = new talk();
 
// 將A類繫結至容器,命名為foo
Container::bind(`foo`, function() {
    return new A;
});
 
// 通過容器取出例項
$talk->greet(Container::make(`foo`)); // Hello World

上述例子中,只有在通過make方法獲取例項的時候,例項才被建立,這樣使得我們可以實現容器。

Laravel框架底層也大量使用了閉包以及bindTo方法,利用好閉包可以實現更多的高階特性如事件觸發等。


以上為閉包學習筆記,部分參考了網上的一些文章。

相關文章