[关闭]
@dungan 2019-03-14T02:12:08.000000Z 字数 2880 阅读 43

PHP

闭包与作用域

虽然闭包在 php 中相对陌生,但由于 laravel 框架使用了大量的闭包,如果想去探索 laravel 的底层源码,还是有必要了解下闭包在 php 中的使用!

概念

闭包是什么

在接触闭包之前我们知道函数要么返回 boolean,要么返回特定值,但是还能返回一种类型:函数

这个被返回的函数就形成了闭包!

这样函数内部的函数自然就能访问到父函数的作用域,那这样一个闭包就可以递归式的包含所有包含它的函数作用域,于是就形成了一个作用域链!

闭包的作用是什么

根据垃圾收集的规律,一个没有被引用的变量会被垃圾收集器清除,这也是为什么通常随着函数执行结束后,函数所在作用域的变量会被销毁,但是如果能继续引用该变量,那该变量不就会被清除,而闭包恰恰能够实现该想法!

我们知道闭包所在的环境形成了作用域链,正是有了这个作用域链,我们就可以在闭包中持有这个作用域链上的任何数据,这对于对改变程序执行的正常流程有非常重要的意义,例如:

  • 访问闭包函数所在所用于内部的私有属性
  • 延长闭包函数作用域中变量的生命周期
  • ...


php 中的闭包

构造这么一个类 Test,有两个私有属性 b

  1. class Test
  2. {
  3. private $a = 'a';
  4. private static $b = 'b';
  5. }

现在我想直接在类外部访问它里面的这两个私有属性那该怎么办?可能有人会想,直接 new 一个 Test 这样访问

  1. $test = new Test();
  2. echo $test->a;

但是很遗憾,不行!!!

长久以来面向对象的灌输给我们私有属性是在类外部是不能被访问的,所以如果你想向上面的一样直接new后访问肯定会报错的!

这里我先立一个flag,我就是想在 Test 类外部的一个独立函数中访问它的私有属性,像下面这样

  1. class Test
  2. {
  3. private $a = 'a';
  4. private static $b = 'b';
  5. }
  6. $closure = function()
  7. {
  8. echo $this->a. PHP_EOL;
  9. echo self::$b;
  10. };

这里把一个匿名函数赋值给一个变量,这和正常申明的函数调用方式一样,通过函数名调用: $closure()

毫无疑问,直接这样调用肯定会报错,因为在一个独立的函数体中,根本不认识 $this 是什么鬼!

那怎么让 this?

答案是通过闭包,还记得之前我们说闭包所在的环境会形一个作用域链么,一旦有这个作用域链,我们就可以访问作用域中的任何数据!

重点就是怎么构造一个持有作用域的闭包出来!!!

其实 php 中的匿名函数(Anonymous functions)就是 闭包函数(closures);

  1. var_dump($closure instanceof Closure); //bool(true)

匿名函数的定义是:

没有指定名称的函数,正如你所见之前将一个没有名称的函数赋值给了 $closure 变量,所以该函数就是一个匿名函数,当然也是闭包函数!

闭包函数 this 是什么,那该怎么办呢?

在 php 中所有的匿名函数都是 Closure 类的实例,该类有两个方法 :『bind()』 与 『bindTo()』!

Closure

Closure 类中这两个方法正如它们名字所示的意思一样,是用来为闭包函数绑定作用域的!

  1. Closure::bind ( Closure $closure , object $newthis [, mixed $newscope = 'static' ] )
  2. Closure::bindTo ( object $newthis [, mixed $newscope = 'static' ] )

这两个函数用法差不多,关于参数 $newthis 和 $newscope 说明如下

如果你想让闭包函数中的 $this 有所指,那么请你传入 $newthis,它是你实例化后要绑定的对象!
如果你想访问绑定对象的私有属性,那么请你传入 $newscope ,它是你要绑定对象的类型名称(也就是__class__或者static::class)!对于公开属性,$newscope 是可选的!

通过 Closure 对象我们就可以给闭包构造作用域,这样 this 终于有所指向,而闭包中也就有了作用域了

综上所述,通过 bind 我们就可以为 this 就有所指了!

bind 绑定作用域

那就用 bind 为 $closure 函数绑定一个作用域吧!

  1. class Test
  2. {
  3. private $a = 'a';
  4. private static $b = 'b';
  5. }
  6. $test = new Test();
  7. $closure = function()
  8. {
  9. echo $this->a. PHP_EOL;
  10. echo self::$b;
  11. };
  12. (Closure::bind($closure, $test, $test))();
  13. // 或者 ($closure->bindTo($test, $test))();
  14. // a
  15. // b

这样通过构造了一个匿名函数 $closure,并为其绑定了 Test 对象的作用域,最终访问到了 Test 对象的私有属性!

你可能对 (Closure::bind(test, $test))() 这种函数执行方式有疑问,首先申明『这是一种合法的语法』

这种语法学名叫做 IIFE(立即调用的函数表达式),意思是定义函数的同时执行它,语法格式为: (function(){... }()),其实这种写法在 javascript 中非常普遍,js 中的模块模式就是通过这种方式构造的!

当然你还可以把绑定和执行拆成两步

  1. class Test
  2. {
  3. private $a = 'a';
  4. private static $b = 'b';
  5. }
  6. $test = new Test();
  7. $closure = function()
  8. {
  9. echo $this->a. PHP_EOL;
  10. echo self::$b;
  11. };
  12. $closure_bind = $closure->bindTo($test, $test); // 绑定
  13. //或者 $closure_bind = Closure::bind($closure, $test, $test);
  14. $closure_bind(); // 执行
  15. // a
  16. // b

上面的几个示例只是为了举例说明,有点简单,其实一般我们是像下面这样这么用闭包的

  1. class Test
  2. {
  3. public $num = 1;
  4. function count($closure)
  5. {
  6. $closure = $closure->bindTo($this, $this);
  7. return $closure(1);
  8. }
  9. }
  10. $test = new Test();
  11. $closure = function($add)
  12. {
  13. return $this->num + $add;
  14. };
  15. echo $test->count($closure); //2

最后

通过 bind 绑定作用域的这个过程其实就是对闭包定义的完美诠释,同时也颠覆了传统的面向对象的灌输给我们私有属性不能在类外部访问的概念,其实闭包某种程度上已经算是函数式编程的范畴了,这自然和面向对象思考模式不一样了,这是种观念的转变!

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注