还不知道PHP有闭包?那你真OUT了做过一段时间的Web开发,我们都知道或者了解JavaScript中有个非常强大的语法,那就是闭包。其实,在PHP中也早就有了闭包函数的功能。早在5.3版本的PHP中,闭包函数就已经出现了。到了7以及后来的现代框架中,闭包函数的使用更是无处不在。在这里,我们就先从基础来了解PHP中闭包的使用吧! 闭包函数(closures)在PHP中都会转换为 Closure 类的实例。在定义时如果是赋值给变量,在结尾的花括号需要添加;分号。闭包函数从父作用域中继承变量,任何此类变量都应该用 use 语言结构传递进去。PHP 7.1 起,不能传入此类变量:superglobals、 $this 或者和参数重名 基础语法
闭包的使用非常简单,和JavaScript也非常相似。因为他们都有另外一个别名,叫做匿名函数。 1$a = function () { 2 echo "this is testA"; 3}; 4$a(); // this is testA 5 6 7function testA ($a) { 8 var_dump($a); 9} 10testA($a); // class Closure#1 (0) {} 11 12$b = function ($name) { 13 echo 'this is ' . $name; 14}; 15 16$b('Bob'); // this is Bob
我们将$a和$b两个变量直接赋值为两个函数。这样我们就可以使用变量()的形式调用这两个函数了。通过testA()方法,我们可以看出闭包函数是可以当做普通参数传递的,因为它自动转换成为了 Closure 类的实例。 1$age = 16; 2$c = function ($name) { 3 echo 'this is ' . $name . ', Age is ' . $age; 4}; 5 6$c('Charles'); // this is Charles, Age is 7 8$c = function ($name) use ($age) { 9 echo 'this is ' . $name . ', Age is ' . $age; 10}; 11 12$c('Charles'); // this is Charles, Age is 16
如果我们需要调用外部的变量,需要使用use关键字来引用外部的变量。这一点和普通函数不一样,因为闭包有着严格的作用域问题。对于全局变量来说,我们可以使用use,也可以使用global。但是对于局部变量(函数中的变量)时,只能使用use。这一点我们后面再说。 作用域
1function testD(){ 2 global $testOutVar; 3 echo $testOutVar; 4} 5$d = function () use ($testOutVar) { 6 echo $testOutVar; 7}; 8$dd = function () { 9 global $testOutVar; 10 echo $testOutVar; 11}; 12$testOutVar = 'this is d'; 13$d(); // NULL 14testD(); // this is d 15$dd(); // this is d 16 17$testOutVar = 'this is e'; 18$e = function () use ($testOutVar) { 19 echo $testOutVar; 20}; 21$e(); // this is e 22 23$testOutVar = 'this is ee'; 24$e(); // this is e 25 26$testOutVar = 'this is f'; 27$f = function () use (&$testOutVar) { 28 echo $testOutVar; 29}; 30$f(); // this is f 31 32$testOutVar = 'this is ff'; 33$f(); // this is ff
在作用域中,use传递的变量必须是在函数定义前定义好的,从上述例子中可以看出。如果闭包($d)是在变量($testOutVar)之前定义的,那么$d中use传递进来的变量是空的。同样,我们使用global来测试,不管是普通函数(testD())或者是闭包函数($dd),都是可以正常使用$testOutVar的。 在$e函数中的变量,在函数定义之后进行修改也不会对$e闭包内的变量产生影响。这时候,必须要使用引用传递($f)进行修改才可以让闭包里面的变量产生变化。这里和普通函数的引用传递与值传递的概念是相同的。 除了变量的use问题,其他方面闭包函数和普通函数基本没什么区别,比如进行类的实例化: 1class G 2{} 3$g = function () { 4 global $age; 5 echo $age; // 16 6 $gClass = new G(); 7 var_dump($gClass); // G info 8}; 9$g();
类中作用域
关于全局作用域,闭包函数和普通函数的区别不大,主要的区别体现在use作为桥梁进行变量传递时的状态。在类方法中,有没有什么不一样的地方呢? 1$age = 18; 2class A 3{ 4 private $name = 'A Class'; 5 public function testA() 6 { 7 $insName = 'test A function'; 8 $instrinsic = function () { 9 var_dump($this); // this info 10 echo $this->name; // A Class 11 echo $age; // NULL 12 echo $insName; // null 13 }; 14 $instrinsic(); 15 16 $instrinsic1 = function () { 17 global $age, $insName; 18 echo $age; // 18 19 echo $insName; // NULL 20 }; 21 $instrinsic1(); 22 23 global $age; 24 $instrinsic2 = function () use ($age, $insName) { 25 echo $age; // 18 26 echo $insName; // test A function 27 }; 28 $instrinsic2(); 29 30 } 31} 32 33$aClass = new A(); 34$aClass->testA();
小技巧
了解了闭包的这些特性后,我们可以来看几个小技巧: 1$arr1 = [ 2 ['name' => 'Asia'], 3 ['name' => 'Europe'], 4 ['name' => 'America'], 5]; 6 7$arr1Params = ' is good!'; 8// foreach($arr1 as $k=>$a){ 9// $arr1[$k] = $a . $arr1Params; 10// } 11// print_r($arr1); 12 13array_walk($arr1, function (&$v) use ($arr1Params) { 14 $v .= ' is good!'; 15}); 16print_r($arr1);
干掉foreach:很多数组类函数,比如array_map、array_walk等,都需要使用闭包函数来处理。上例中我们就是使用array_walk来对数组中的内容进行处理。是不是很有函数式编程的感觉,而且非常清晰明了。 1function testH() 2{ 3 return function ($name) { 4 echo "this is " . $name; 5 }; 6} 7testH()("testH's closure!"); // this is testH's closure!
看到这样的代码也不要懵圈了。PHP7支持立即执行语法,也就是JavaScript中的IIFE(Immediately-invoked function expression)。 我们再来一个计算斐波那契数列的: 1$fib = function ($n) use (&$fib) { 2 if ($n == 0 || $n == 1) { 3 return 1; 4 } 5 6 return $fib($n - 1) + $fib($n - 2); 7}; 8 9echo $fib(10);
同样的还是使用递归来实现。这里直接换成了闭包递归来实现。最后有一点要注意的是,use中传递的变量名不能是带下标的数组项: 1$fruits = ['apples', 'oranges']; 2$example = function () use ($fruits[0]) { // Parse error: syntax error, unexpected '[', expecting ',' or ')' 3 echo $fruits[0]; 4}; 5$example();
这样写直接就是语法错误,无法成功运行的。 彩蛋
Laravel中的IoC服务容器中,大量使用了闭包能力,我们模拟一个便于大家理解。当然,更好的方案是自己去翻翻Laravel的源码。 1class B 2{} 3class C 4{} 5class D 6{} 7class Ioc 8{ 9 public $objs = []; 10 public $containers = []; 11 12 public function __construct() 13 { 14 $this->objs['b'] = function () { 15 return new B(); 16 }; 17 $this->objs['c'] = function () { 18 return new C(); 19 }; 20 $this->objs['d'] = function () { 21 return new D(); 22 }; 23 } 24 public function bind($name) 25 { 26 if (!isset($this->containers[$name])) { 27 if (isset($this->objs[$name])) { 28 $this->containers[$name] = $this->objs[$name](); 29 } else { 30 return null; 31 } 32 } 33 return $this->containers[$name]; 34 } 35} 36 37$ioc = new Ioc(); 38$bClass = $ioc->bind('b'); 39$cClass = $ioc->bind('c'); 40$dClass = $ioc->bind('d'); 41$eClass = $ioc->bind('e'); 42 43var_dump($bClass); // B 44var_dump($cClass); // C 45var_dump($dClass); // D 46var_dump($eClass); // NULL
总结
闭包特性经常出现的地方是事件回调类的功能中,另外就是像彩蛋中的IoC的实现。因为闭包有一个很强大的能力就是可以延迟加载。IoC的例子我们的闭包中返回的是新new出来的对象。当我们的程序运行的时候,如果没有调用$ioc->bind('b'),那么这个B对象是不会创建的,也就是说这时它还不会占用资源占用内存。而当我们需要的时候,从服务容器中拿出来的时候才利用闭包真正的去创建对象。同理,事件的回调也是一样的概念。事件发生时在我们需要处理的时候才去执行回调里面的代码。如果没有闭包的概念,那么$objs容器就这么写了: 1$this->objs['b'] = new B(); 2$this->objs['c'] = new C(); 3$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./manual/zh/functions.anonymous.php https://www./manual/zh/functions.anonymous.php#100545 https://www./manual/zh/functions.anonymous.php#119388
|