PHP 闭包


PHP 闭包


正文

匿名函数和闭包并没有什么直接关系

匿名函数

什么是匿名函数?

与匿名函数相对应的是具名函数,具名函数非常简单:function myFn(){},这就是个具名函数这个函数的name是myFn。

PHP声明一个匿名函数是这样:

$func = function() {
    
}; //带结束符

可以看到,匿名函数因为没有名字,如果要使用它,需要将其返回给一个变量。 匿名函数也像普通函数一样可以声明参数,调用方法也相同:

$func = function($param) {
    echo $param;
};

$func('some string');

//输出:
//some string

闭包

闭包是怎么定义的,该如何理解?

闭包本身定义比较抽象,MDN官方上解释是: A closure is the combination of a function and the lexical environment within which that function was declared.

中文解释是:闭包是一个函数和该函数被定义时的词法环境的组合。

按照闭包起的作用来理解它:就是能在一个函数外部执行这个函数内部定义的方法,并访问这个函数内部定义的变量。

闭包跟函数是否匿名没有直接关系,匿名函数和具名函数都可以创建闭包。

function box(){
  var a = 10;
  function inner(){
    return a;
  }
  return inner;
}
var outer = box();
console.log(outer());//10
//第一步直把内部inner这个具名函数改为匿名函数并直接return, 结果同样是10
function box(){
  var a = 10;
  return function(){
    console.log(a) ; 
  }
}
var outer = box();
outer();//10
//第二步把外部var outer = box()改成立即执行的匿名函数
var outer = (function(){
  var a=10;
  return function(){
    console.log(a);
  }
})();
//outer 作为立即执行匿名函数执行结果的一个接收,这个执行结果是闭包,outer等于这个闭包。
//执行outer就相当于执行了匿名函数内部return的闭包函数
//这个闭包函数可以访问到匿名函数内部的私有变量a,所以打印出10
outer();//10

实现闭包

将匿名函数在普通函数中当做参数传入,也可以被返回。这就实现了一个简单的闭包。

//例一
//在函数里定义一个匿名函数,并且调用它
function printStr() {
    $func = function( $str ) {
        echo $str;
    };
    $func( 'some string' );
}

printStr();



//例二
//在函数中把匿名函数返回,并且调用它
function getPrintStrFunc() {
    $func = function( $str ) {
        echo $str;
    };
    return $func;
}

$printStrFunc = getPrintStrFunc();
$printStrFunc( 'some string' );




//例三
//把匿名函数当做参数传递,并且调用它
function callFunc( $func ) {
    $func( 'some string' );
}

$printStrFunc = function( $str ) {
    echo $str;
};
callFunc( $printStrFunc );

//也可以直接将匿名函数进行传递。如果你了解js,这种写法可能会很熟悉
callFunc( function( $str ) {
    echo $str;
} );

USE 关键字

连接闭包和外界变量的关键字:USE,use意思是连接闭包和外界变量。

闭包可以保存所在代码块上下文的一些变量和值。PHP在默认情况下,匿名函数不能调用所在代码块的上下文变量,而需要通过使用use关键字。

换一个例子看看:

function getMoney() {
    $rmb = 1;
    $dollar = 6;
    $func = function() use ( $rmb ) {
        echo $rmb;
        echo $dollar;
    };
    $func();
}

getMoney();

//输出:
//1
//报错,找不到dorllar变量

可以看到,dollar没有在use关键字中声明,在这个匿名函数里也就不能获取到它,所以开发中要注意这个问题。

在匿名函数中不能改变上下文的变量,use所引用的也只不过是变量的一个副本而已:

function getMoney() {
    $rmb = 1;
    $func = function() use ( $rmb ) {
        echo $rmb;
        //把$rmb的值加1
        $rmb++;
    };
    $func();
    echo $rmb;
}

getMoney();

//输出:
//1
//1

要完全引用变量,而不是复制,需要按地址&引用:

function getMoney() {
    $rmb = 1;
    $func = function() use ( &$rmb ) {
        echo $rmb;
        //把$rmb的值加1
        $rmb++;
    };
    $func();
    echo $rmb;
}

getMoney();

//输出:
//1
//2

如果将匿名函数返回给外界,匿名函数会保存use所引用的变量,而外界则不能得到这些变量:

function getMoneyFunc() {
    $rmb = 1;
    $func = function() use ( &$rmb ) {
        echo $rmb;
        //把$rmb的值加1
        $rmb++;
    };
    return $func;
}

$getMoney = getMoneyFunc();
$getMoney();
$getMoney();
$getMoney();

//输出:
//1
//2
//3

闭包的几个作用

减少foreach的循环的代码

<?php
// 一个基本的购物车,包括一些已经添加的商品和每种商品的数量。
// 其中有一个方法用来计算购物车中所有商品的总价格。该方法使用了一个closure作为回调函数。
class Cart
{
    const PRICE_BUTTER  = 1.00;
    const PRICE_MILK    = 3.00;
    const PRICE_EGGS    = 6.95;
 
    protected   $products = array();
     
    public function add($product, $quantity)
    {
        $this->products[$product] = $quantity;
    }
     
    public function getQuantity($product)
    {
        return isset($this->products[$product]) ? $this->products[$product] :
               FALSE;
    }
     
    public function getTotal($tax)
    {
        $total = 0.00;
         
        $callback =
            function ($quantity, $product) use ($tax, &$total)
            {
                $pricePerItem = constant(__CLASS__ . "::PRICE_" .
                    strtoupper($product));
                $total += ($pricePerItem * $quantity) * ($tax + 1.0);
            };
         
        array_walk($this->products, $callback);
        return round($total, 2);;
    }
}
 
$my_cart = new Cart;
 
// 往购物车里添加条目
$my_cart->add('butter', 1);
$my_cart->add('milk', 3);
$my_cart->add('eggs', 6);
 
// 打出出总价格,其中有 5% 的销售税.
print $my_cart->getTotal(0.05) . "\n";
// The result is 54.29

减少函数的参数

function html ($code , $id="", $class="") {
    if ($id !== "") {
        $id = " id = \"$id\"";
    }
    
    $class = ($class !== "") ? " class =\"$class\"" : ">";
    $open = "<$code$id$class";
    $close = "</$code>";
    return function ($inner = "") use ($open, $close){
        return "$open$inner$close";
    };
}

如果是使用平时的方法,我们会把inner放到html函数参数中,这样不管是代码阅读还是使用都不如使用闭包

解除递归函数

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

echo $fib(2) . "\n"; // 2
$lie = $fib;
$fib = function(){die('error');};//rewrite $fib variable 
echo $lie(5); // error   because $fib is referenced by closure
$fib = function(){echo 3;};
echo $lie(1);   // 1
echo $lie(2);   // 330
echo $lie(3);   // 330

注意上题中的use使用了&,这里不使用&会出现错误fib(n-1)是找不到function的(前面没有定义fib的类型)

所以想使用闭包解除循环函数的时候就需要使用这样的形式:

<?php
$recursive = function () use (&$recursive){
// The function is now available as $recursive
}

关于延迟绑定

如果你需要延迟绑定use里面的变量,你就需要使用引用,否则在定义的时候就会做一份拷贝放到use中:

<?php
$result = 0;
 
$one = function()
{ 
    var_dump($result); 
};
 
$two = function() use ($result)
{ 
    var_dump($result); 
};
 
$three = function() use (&$result)
{ 
    var_dump($result); 
};
 
$result++;
 
$one();    // outputs NULL: $result is not in scope
$two();    // outputs int(0): $result was copied
$three();    // outputs int(1)
$two();    // outputs int(0): 

使用引用和不使用引用就代表了是调用时赋值,还是申明时候赋值。

Closure类

Closure类原型:

/**
 * Class used to represent anonymous functions.
 * <p>Anonymous functions, implemented in PHP 5.3, yield objects of this type.
 * This fact used to be considered an implementation detail, but it can now be relied upon.
 * Starting with PHP 5.4, this class has methods that allow further control of the anonymous function after it has been created.
 * <p>Besides the methods listed here, this class also has an __invoke method.
 * This is for consistency with other classes that implement calling magic, as this method is not used for calling the function.
 * @link https://secure.php.net/manual/en/class.closure.php
 */
final class Closure {

    /**
     * This method exists only to disallow instantiation of the Closure class.
     * Objects of this class are created in the fashion described on the anonymous functions page.
     * @link https://secure.php.net/manual/en/closure.construct.php
     */
    private function __construct() { }

    /**
     * This is for consistency with other classes that implement calling magic,
     * as this method is not used for calling the function.
     * @param mixed ...$_ [optional]
     * @return mixed
     * @link https://secure.php.net/manual/en/class.closure.php
     */
    public function __invoke(...$_) { }

    /**
     * Duplicates the closure with a new bound object and class scope
     * @link https://secure.php.net/manual/en/closure.bindto.php
     * @param object|null $newThis The object to which the given anonymous function should be bound, or NULL for the closure to be unbound.
     * @param mixed $newScope The class scope to which associate the closure is to be associated, or 'static' to keep the current one.
     * If an object is given, the type of the object will be used instead.
     * This determines the visibility of protected and private methods of the bound object.
     * @return Closure|false Returns the newly created Closure object or FALSE on failure
     */
    function bindTo($newThis, $newScope = 'static') { }

    /**
     * This method is a static version of Closure::bindTo().
     * See the documentation of that method for more information.
     * @link https://secure.php.net/manual/en/closure.bind.php
     * @param Closure $closure The anonymous functions to bind.
     * @param object|null $newThis The object to which the given anonymous function should be bound, or NULL for the closure to be unbound.
     * @param mixed $newScope The class scope to which associate the closure is to be associated, or 'static' to keep the current one.
     * If an object is given, the type of the object will be used instead.
     * This determines the visibility of protected and private methods of the bound object.
     * @return Closure|false Returns the newly created Closure object or FALSE on failure
     */
    static function bind(Closure $closure, $newThis, $newScope = 'static') { }

    /**
     * Temporarily binds the closure to newthis, and calls it with any given parameters.
     * @link https://php.net/manual/en/closure.call.php
     * @param object $newThis The object to bind the closure to for the duration of the call.
     * @param mixed $args [optional] Zero or more parameters, which will be given as parameters to the closure.
     * @return mixed
     * @since 7.0
     */
    function call ($newThis, ...$args) {}

    /**
     * @param callable $callback
     * @return Closure
     * @since 7.1
     */
    public static function fromCallable (callable $callback) {}
}

Closure::__construct — 用于禁止实例化的构造函数。

Closure::bind — 复制一个闭包,绑定指定的$this对象和类作用域。

Closure::bindTo — 复制当前闭包对象,绑定指定的$this对象和类作用域。

Closure::bind

Closure::bind是Closure::bindTo的静态版本,其说明如下:

public static Closure bind(Closure $closure, $newThis, $newScope = 'static') { }

closure表示需要绑定的闭包对象。

newthis表示需要绑定到闭包对象的对象,或者NULL创建未绑定的闭包。

newscope表示想要绑定给闭包的类作用域,可以传入类名或类的示例,默认值是 ‘static’, 表示不改变。

该方法成功时返回一个新的 Closure 对象,失败时返回FALSE。

例子说明:

<?php
/** 
 * 复制一个闭包,绑定指定的$this对象和类作用域。 
 * 
 * @author 疯狂老司机 
 */
class Animal 
{
    private static $cat = "cat";
    private $dog = "dog";
    public $pig = "pig";
}
 
/* 
 * 获取Animal类静态私有成员属性
 */
$cat = static function() {
    return Animal::$cat;
};
 
/* 
 * 获取Animal实例私有成员属性
 */
$dog = function() {
    return $this->dog;
};
 
/* 
 * 获取Animal实例公有成员属性
 */
$pig = function() {
    return $this->pig;
};
 
$bindCat = Closure::bind($cat, null, new Animal());// 给闭包绑定了Animal实例的作用域,但未给闭包绑定$this对象
$bindDog = Closure::bind($dog, new Animal(), 'Animal');// 给闭包绑定了Animal类的作用域,同时将Animal实例对象作为$this对象绑定给闭包
$bindPig = Closure::bind($pig, new Animal());// 将Animal实例对象作为$this对象绑定给闭包,保留闭包原有作用域
echo $bindCat(),'<br>';// 根据绑定规则,允许闭包通过作用域限定操作符获取Animal类静态私有成员属性
echo $bindDog(),'<br>';// 根据绑定规则,允许闭包通过绑定的$this对象(Animal实例对象)获取Animal实例私有成员属性
echo $bindPig(),'<br>';// 根据绑定规则,允许闭包通过绑定的$this对象获取Animal实例公有成员属性

输出:

cat
dog
pig

Closure::bindTo说明

Closure::bindTo — 复制当前闭包对象,绑定指定的$this对象和类作用域,其说明如下:

public Closure bindTo($newThis, $newScope = 'static') { }

newthis表示绑定给闭包对象的一个对象,或者NULL来取消绑定。

newscope表示关联到闭包对象的类作用域,可以传入类名或类的示例,默认值是 ‘static’, 表示不改变。

该方法创建并返回一个闭包对象,它与当前对象绑定了同样变量,但可以绑定不同的对象,也可以绑定新的类作用域。 绑定的对象决定了返回的闭包对象中的$this的取值,类作用域决定返回的闭包对象能够调用哪些方法, 也就是说,此时$this可以调用的方法,与newscope类作用域相同。

例子1

Template.php 模板类

<?php
/** 
 * 模板类,用于渲染输出 
 */
class Template{
    /**
     * 渲染方法
     *
     * @access public 
     * @param obj 信息类
     * @param string 模板文件名
     */
    public function render($context, $tpl){
        $closure = function($tpl){
            ob_start();
            include $tpl;
            return ob_end_flush();
        };
        $closure = $closure->bindTo($context, $context);
        $closure($tpl);
    }
}

Article.php 信息类

<?php
/** 
 * 文章信息类 
 */
class Article{
    private $title = "这是文章标题";
    private $content = "这是文章内容";
}

tpl.php 模板文件

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
    <head>
        <meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
    </head>
    <body>
        <h1><?php echo $this->title;?></h1>
        <p><?php echo $this->content;?></p>
    </body>
</html>

使用调用:

<?php
function __autoload($class) {
    require_once "$class.php";
}
 
$template = new Template;
$template->render(new Article, 'tpl.php');
?>

例子2

<?php
/** 
 * 给类动态添加新方法
 */
trait DynamicTrait {
 
    /**
     * 自动调用类中存在的方法
     */
    public function __call($name, $args) {
        if(is_callable($this->$name)){
            return call_user_func($this->$name, $args);
        }else{
            throw new \RuntimeException("Method {$name} does not exist");
        }
    }
    /**
     * 添加方法
     */
    public function __set($name, $value) {
        $this->$name = is_callable($value)? 
            $value->bindTo($this, $this): 
            $value;
    }
}
 
/** 
 * 只带属性不带方法动物类
 */
class Animal {
    use DynamicTrait;
    private $dog = 'dog';
}
 
$animal = new Animal;
 
// 往动物类实例中添加一个方法获取实例的私有属性$dog
$animal->getdog = function() {
    return $this->dog;
};
 
echo $animal->getdog();
 
?>

输出: dog

例子3

<?php
/** 
 * 一个基本的购物车,包括一些已经添加的商品和每种商品的数量
 */
class Cart {
    // 定义商品价格
    const PRICE_BUTTER  = 1.00;
    const PRICE_MILK    = 3.33;
    const PRICE_EGGS    = 8.88;
 
    protected  $products = array();
 
    /**
     * 添加商品和数量
     *
     * @access public 
     * @param string 商品名称
     * @param string 商品数量
     */
    public function add($item, $quantity) {
        $this->products[$item] = $quantity;
    }
 
    /**
     * 获取单项商品数量
     *
     * @access public 
     * @param string 商品名称
     */
    public function getQuantity($item) {
        return isset($this->products[$item]) ? $this->products[$item] : FALSE;
    }
 
    /**
     * 获取总价
     *
     * @access public 
     * @param string 税率
     */
    public function getTotal($tax) {
        $total = 0.00;
 
        $callback = function ($quantity, $item) use ($tax, &$total) {
            $pricePerItem = constant(__CLASS__ . "::PRICE_" . strtoupper($item));
            $total += ($pricePerItem * $quantity) * ($tax + 1.0);
        };
         
        array_walk($this->products, $callback);
        return round($total, 2);;
    }
}
 
$my_cart = new Cart;
 
// 往购物车里添加商品及对应数量
$my_cart->add('butter', 10);
$my_cart->add('milk', 3);
$my_cart->add('eggs', 12);
 
// 打出出总价格,其中有 5% 的销售税.
echo $my_cart->getTotal(0.05);

总结:合理使用闭包能使代码更加简洁和精炼。






参考资料

PHP 手册 语言参考 函数 匿名函数 https://www.php.net/manual/zh/functions.anonymous.php

PHP闭包(Closure)初探 https://www.cnblogs.com/melonblog/archive/2013/05/01/3052611.html

PHP的闭包 https://www.cnblogs.com/yjf512/archive/2012/10/29/2744702.html

PHP Closure类详解 https://blog.csdn.net/wuxing26jiayou/article/details/51067190

浅谈匿名函数和闭包 https://www.jianshu.com/p/0a3150afb7ed


返回