PHP 函数(下):匿名函数和作用域

匿名函数

从 PHP 5.3 开始,引入了对匿名函数的支持,所谓匿名函数就是在函数定义中没有显式声明函数名,在 PHP 中,匿名函数也被称作闭包函数(Closure)。

编写匿名函数

我们在 php_learning/function 目录下创建 closure.php 来存放本篇教程编写的代码。 以上篇教程演示的自定义函数 add 为例,如果通过匿名函数进行定义,就是这样的:

-w1006

上面第一个红色方框里面是匿名函数的定义部分,可以看到在 function 之后没有声明函数名,而是将整个函数赋值给了 $add 变量(不要漏掉赋值语句最后的分号),这样,$add 就变成了函数类型,也因此,函数在 PHP 中也可以看作是一等公民(first class),可以赋值给变量进行调用,此时,如果我们试图通过 var_dump($add) 打印 $add,结果如下:

-w630

可以看到它的类型是用于代表匿名函数的 Closure 类,并且该匿名函数支持两个必填参数 $a$b

回到 closure.php,在上述截图的第二个红色方框区域是匿名函数的调用部分,我们可以直接将 $add 作为一个函数名进行调用,打印结果是:

1 + 2 = 3

此外,还可以通过 PHP 内置的 call_user_func 函数调用该函数,第一个参数是函数名,后面的参数是函数参数(非匿名函数亦可通过 call_user_func 函数调用):

$sum = call_user_func($add, $a, $b);

返回结果和上面的 $add($a, $b) 完全一致。

可变数量的参数列表

如果感兴趣的话,看 call_user_func 函数的声明:

function call_user_func ($function, ...$parameter)

可以看到代表参数列表的 $parameter 前面有一个 ... 前缀,其作用是标识该参数是一个可变数量的参数列表,也就是支持传入任意多个参数,从 0~N 个不等,比如我们这里传入的就是 $a$b 两个参数,如果待调用函数 $function 不需要传递参数,则 $parameter 部分留空,如果只需要传入一个参数,则传入一个参数,依此类推。

默认参数

说到这里,我们还可以为函数设置默认参数,即为指定参数设置默认值,需要注意的是默认参数需要放到参数列表最后:

$add = function (int $a, int $b = 2): int {
    return $a + $b;
};

这个时候,调用 $add 函数就可以不传入第二个参数了,该参数会使用默认参数值:

$n1 = 1;
$n2 = 2;
$sum = $add($n1);
echo "$n1 + $n2 = $sum" . PHP_EOL;

当然,你可以可以传入第二个参数覆盖默认值:

$n1 = 1;
$n2 = 3;
$sum = $add($n1, $n2);
echo "$n1 + $n2 = $sum" . PHP_EOL;

这样打印的结果就变成了:

1 + 3 = 4

可变函数

最后,由于 $add 是一个函数类型变量,并且 PHP 是动态类型语言,所以我们还可以像操作基本类型变量那样将其他函数类型值赋值给 $add,这些函数类型值包括匿名函数和非匿名函数,比如我们新增一个两数相乘函数 multi,然后在运行时将其赋值给 $add

-w648

注意第二个红色方框,我们在运行时将 multi 函数赋值给 $add,再调用 $add($n1, $n2) 则等同于调用 multi($n1, $n2),当然如果通过匿名函数定义 multi 也是可以的,对应的实现代码如下:

<?php

/**
 * 通过匿名函数定义两数相加函数 add
 * @param int $a
 * @param int $b
 * @return int
 */
$add = function (int $a, int $b = 2): int {
    return $a + $b;
};

/**
 * 两数相乘函数 multi
 * @param int $a
 * @param int $b
 * @return int
 */
$multi = function (int $a, int $b): int {
    return $a * $b;
};

// 调用匿名函数
$n1 = 1;
$n2 = 3;
$sum = $add($n1, $n2);
echo "$n1 + $n2 = $sum" . PHP_EOL;
// 将 multi 赋值给 $add
$add = $multi;
$product = $add($n1, $n2);
echo "$n1 x $n2 = $product" . PHP_EOL;

打印结果都是一样的:

-w488

这种在运行时动态设置函数类型值给变量的功能,在 PHP 中称之为可变函数。

作用域

继承父作用域变量

匿名函数(或者叫闭包函数)的一个强大功能是支持在函数体中直接引用上下文变量(继承父作用域的变量),比如在上述代码中,我们可以这样编写匿名函数实现代码:

<?php
$n1 = 1;
$n2 = 3;

// 计算两数相加
$add = function () use ($n1, $n2) {
    return $n1 + $n2;
};

// 计算两数相乘
$multi = function () use ($n1, $n2){
    return $n1 * $n2;
};

// 调用匿名函数
$sum = $add();
echo "$n1 + $n2 = $sum" . PHP_EOL;
$product = $multi();
echo "$n1 x $n2 = $product" . PHP_EOL;

只需要通过 use 关键字传递当前上下文中的变量,它们就可以在闭包函数体中直接使用,而不需要通过参数形式传入,这样一来,其他引用该文件的代码就可以间接引用当前父作用域下的变量,如果是在类方法中定义的匿名函数,则可以直接引用相应类实例的属性,关于这一块,学院君会在后续面向对象编程中详细介绍。

通过 global 声明全局变量

如果不是通过匿名函数的话,只能基于 global 关键字通过全局变量引用函数体外部定义的变量:

// 计算两数相减
function sub() {
    global $n1, $n2;
    return $n1 - $n2;
}

global vs. 匿名函数

从父作用域中继承变量与使用全局变量是不同的,全局变量存在于一个全局的范围,无论当前在执行的是哪个函数,而闭包的父作用域是定义该闭包的函数,不一定是调用它的函数。

我们编写一段示例代码来详细解释:

function add1($n1, $n2) {
    return function () use ($n1, $n2) {
        return $n1 + $n2;
    };
}

function add2() {
    return function () {
        global $n1, $n2, $n3;
        return $n1 + $n2 + $n3;
    };
}

$n1 = 1;
$n2 = 3;
$n3 = 4;
$add = add1($n1, $n2);
$sum = $add();
echo "$n1 + $n2 = $sum" . PHP_EOL;

$add = add2();
$sum = $add();
echo "$n1 + $n2 + $n3 = $sum" . PHP_EOL;

在上述代码中,add1 中定义的闭包函数通过 use 引用了父作用域下的 $n1$n2 变量,对于该闭包函数来说,其作用域是 add1 函数,而非调用它的位置,所以如果我们试图在 add1 中定义的闭包函数中通过 use 引用 $n3 会报错。

add2 中定义的闭包函数通过 global 关键字以全局变量的方式引用 $n1$n2$n3,全局变量存在于全局范围,与调用位置无关,所以可以成功引用。

上述代码的执行结果是:

-w618

global 的安全隐患

但实际编码中,尽量避免使用 global 关键字,因为一旦声明了全局变量,就可以在任何位置获取到这些全局变量,非常容易导致系统被攻击,比如我们新增一个函数 test,在这个函数内部就可以试图通过 $GLOBALS 获取对应全局变量:

function test() {
    printf("n1 = %d, n2 = %d, n3 = %d\n", $GLOBALS['n1'], $GLOBALS['n2'], $GLOBALS['n3']);
}

匿名函数则有效规避了这种安全隐患。此外,匿名函数的另一个典型应用场景就是兜底处理(fallback),关于这一点,学院君将在作业项目中演示。

上一篇: PHP 函数(上):自定义函数和内置函数

下一篇: PHP 类与对象、访问控制