做過一段時間的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
關注公眾號:【硬核專案經理】獲取最新文章
添加微信/QQ好友:【xiaoyuezigonggong/149844827】免費得PHP、專案管理學習資料
知乎、公眾號、抖音、頭條搜索【硬核專案經理】
B站ID:482780532
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/250458.html
標籤:PHP
