還不知道PHP有閉包?那你真OUT了

  • 2019 年 12 月 19 日
  • 筆記

還不知道PHP有閉包?那你真OUT了

做過一段時間的Web開發,我們都知道或者了解JavaScript中有個非常強大的語法,那就是閉包。其實,在PHP中也早就有了閉包函數的功能。早在5.3版本的PHP中,閉包函數就已經出現了。到了7以及後來的現代框架中,閉包函數的使用更是無處不在。在這裡,我們就先從基礎來了解PHP中閉包的使用吧!

閉包函數(closures)在PHP中都會轉換為 Closure 類的實例。在定義時如果是賦值給變量,在結尾的花括號需要添加;分號。閉包函數從父作用域中繼承變量,任何此類變量都應該用 use 語言結構傳遞進去。PHP 7.1 起,不能傳入此類變量:superglobals、 $this 或者和參數重名

基礎語法

閉包的使用非常簡單,和JavaScript也非常相似。因為他們都有另外一個別名,叫做匿名函數。

$a = function () {      echo "this is testA";  };  $a(); // this is testA      function testA ($a) {      var_dump($a);  }  testA($a); // class Closure#1 (0) {}    $b = function ($name) {      echo 'this is ' . $name;  };    $b('Bob'); // this is Bob  

我們將$a和$b兩個變量直接賦值為兩個函數。這樣我們就可以使用變量()的形式調用這兩個函數了。通過testA()方法,我們可以看出閉包函數是可以當做普通參數傳遞的,因為它自動轉換成為了 Closure 類的實例。

$age = 16;  $c = function ($name) {      echo 'this is ' . $name . ', Age is ' . $age;  };    $c('Charles'); // this is Charles, Age is    $c = function ($name) use ($age) {      echo 'this is ' . $name . ', Age is ' . $age;  };    $c('Charles'); // this is Charles, Age is 16  

如果我們需要調用外部的變量,需要使用use關鍵字來引用外部的變量。這一點和普通函數不一樣,因為閉包有着嚴格的作用域問題。對於全局變量來說,我們可以使用use,也可以使用global。但是對於局部變量(函數中的變量)時,只能使用use。這一點我們後面再說。

作用域

function testD(){      global $testOutVar;      echo $testOutVar;  }  $d = function () use ($testOutVar) {      echo $testOutVar;  };  $dd = function () {      global $testOutVar;      echo $testOutVar;  };  $testOutVar = 'this is d';  $d(); // NULL  testD(); // this is d  $dd(); // this is d    $testOutVar = 'this is e';  $e = function () use ($testOutVar) {      echo $testOutVar;  };  $e(); // this is e    $testOutVar = 'this is ee';  $e(); // this is e    $testOutVar = 'this is f';  $f = function () use (&$testOutVar) {      echo $testOutVar;  };  $f(); // this is f    $testOutVar = 'this is ff';  $f(); // this is ff  

在作用域中,use傳遞的變量必須是在函數定義前定義好的,從上述例子中可以看出。如果閉包($d)是在變量($testOutVar)之前定義的,那麼$d中use傳遞進來的變量是空的。同樣,我們使用global來測試,不管是普通函數(testD())或者是閉包函數($dd),都是可以正常使用$testOutVar的。

在$e函數中的變量,在函數定義之後進行修改也不會對$e閉包內的變量產生影響。這時候,必須要使用引用傳遞($f)進行修改才可以讓閉包裏面的變量產生變化。這裡和普通函數的引用傳遞與值傳遞的概念是相同的。

除了變量的use問題,其他方面閉包函數和普通函數基本沒什麼區別,比如進行類的實例化:

class G  {}  $g = function () {      global $age;      echo $age; // 16      $gClass = new G();      var_dump($gClass); // G info  };  $g();  

類中作用域

關於全局作用域,閉包函數和普通函數的區別不大,主要的區別體現在use作為橋樑進行變量傳遞時的狀態。在類方法中,有沒有什麼不一樣的地方呢?

$age = 18;  class A  {      private $name = 'A Class';      public function testA()      {          $insName = 'test A function';          $instrinsic = function () {              var_dump($this); // this info              echo $this->name; // A Class              echo $age; // NULL              echo $insName; // null          };          $instrinsic();            $instrinsic1 = function () {              global $age, $insName;              echo $age; // 18              echo $insName; // NULL          };          $instrinsic1();            global $age;          $instrinsic2 = function () use ($age, $insName) {              echo $age; // 18              echo $insName; // test A function          };          $instrinsic2();        }  }    $aClass = new A();  $aClass->testA();  
  • A::testA()方法中的$insName變量,我們只能通過use來拿到。
  • 閉包函數中的$this是調用它的環境的上下文,在這裡就是A類本身。閉包的父作用域是定義該閉包的函數(不一定是調用它的函數)。靜態閉包函數無法獲得$this。
  • 全局變量依然可以使用global獲得。

小技巧

了解了閉包的這些特性後,我們可以來看幾個小技巧:

$arr1 = [      ['name' => 'Asia'],      ['name' => 'Europe'],      ['name' => 'America'],  ];    $arr1Params = ' is good!';  // foreach($arr1 as $k=>$a){  //     $arr1[$k] = $a . $arr1Params;  // }  // print_r($arr1);    array_walk($arr1, function (&$v) use ($arr1Params) {      $v .= ' is good!';  });  print_r($arr1);  

幹掉foreach:很多數組類函數,比如array_map、array_walk等,都需要使用閉包函數來處理。上例中我們就是使用array_walk來對數組中的內容進行處理。是不是很有函數式編程的感覺,而且非常清晰明了。

function testH()  {      return function ($name) {          echo "this is " . $name;      };  }  testH()("testH's closure!"); // this is testH's closure!  

看到這樣的代碼也不要懵圈了。PHP7支持立即執行語法,也就是JavaScript中的IIFE(Immediately-invoked function expression)。

我們再來一個計算斐波那契數列的:

$fib = function ($n) use (&$fib) {      if ($n == 0 || $n == 1) {          return 1;      }        return $fib($n - 1) + $fib($n - 2);  };    echo $fib(10);  

同樣的還是使用遞歸來實現。這裡直接換成了閉包遞歸來實現。最後有一點要注意的是,use中傳遞的變量名不能是帶下標的數組項:

$fruits = ['apples', 'oranges'];  $example = function () use ($fruits[0]) { // Parse error: syntax error, unexpected '[', expecting ',' or ')'      echo $fruits[0];  };  $example();  

這樣寫直接就是語法錯誤,無法成功運行的。

彩蛋

Laravel中的IoC服務容器中,大量使用了閉包能力,我們模擬一個便於大家理解。當然,更好的方案是自己去翻翻Laravel的源碼。

class B  {}  class C  {}  class D  {}  class Ioc  {      public $objs = [];      public $containers = [];        public function __construct()      {          $this->objs['b'] = function () {              return new B();          };          $this->objs['c'] = function () {              return new C();          };          $this->objs['d'] = function () {              return new D();          };      }      public function bind($name)      {          if (!isset($this->containers[$name])) {              if (isset($this->objs[$name])) {                  $this->containers[$name] = $this->objs[$name]();              } else {                  return null;              }          }          return $this->containers[$name];      }  }    $ioc = new Ioc();  $bClass = $ioc->bind('b');  $cClass = $ioc->bind('c');  $dClass = $ioc->bind('d');  $eClass = $ioc->bind('e');    var_dump($bClass); // B  var_dump($cClass); // C  var_dump($dClass); // D  var_dump($eClass); // NULL  

總結

閉包特性經常出現的地方是事件回調類的功能中,另外就是像彩蛋中的IoC的實現。因為閉包有一個很強大的能力就是可以延遲加載。IoC的例子我們的閉包中返回的是新new出來的對象。當我們的程序運行的時候,如果沒有調用$ioc->bind('b'),那麼這個B對象是不會創建的,也就是說這時它還不會佔用資源佔用內存。而當我們需要的時候,從服務容器中拿出來的時候才利用閉包真正的去創建對象。同理,事件的回調也是一樣的概念。事件發生時在我們需要處理的時候才去執行回調裏面的代碼。如果沒有閉包的概念,那麼$objs容器就這麼寫了:

$this->objs['b'] = new B();  $this->objs['c'] = new C();  $this->objs['d'] = new D();  

容器在實例化的時候就把所有的類都必須實例化了。這樣對於程序來說很多用不上的對象就都被創建了,帶來非常大的資源浪費。

基於閉包的這種強大能力,現在閉包函數已經在Laravel、TP6等框架中無處不在了。學習無止盡,掌握原理再去學習框架往往更能事半功倍。

測試代碼: https://github.com/zhangyue0503/dev-blog/blob/master/php/201911/source/%E8%BF%98%E4%B8%8D%E7%9F%A5%E9%81%93PHP%E6%9C%89%E9%97%AD%E5%8C%85%EF%BC%9F%E9%82%A3%E4%BD%A0%E7%9C%9FOUT%E4%BA%86.php

參考文檔: https://www.php.net/manual/zh/functions.anonymous.php https://www.php.net/manual/zh/functions.anonymous.php#100545 https://www.php.net/manual/zh/functions.anonymous.php#119388