PHP 函数(下):匿名函数和作用域
匿名函数
从 PHP 5.3 开始,引入了对匿名函数的支持,所谓匿名函数就是在函数定义中没有显式声明函数名,在 PHP 中,匿名函数也被称作闭包函数(Closure)。
编写匿名函数
我们在 php_learning/function
目录下创建 closure.php
来存放本篇教程编写的代码。 以上篇教程演示的自定义函数 add
为例,如果通过匿名函数进行定义,就是这样的:
上面第一个红色方框里面是匿名函数的定义部分,可以看到在 function
之后没有声明函数名,而是将整个函数赋值给了 $add
变量(不要漏掉赋值语句最后的分号),这样,$add
就变成了函数类型,也因此,函数在 PHP 中也可以看作是一等公民(first class),可以赋值给变量进行调用,此时,如果我们试图通过 var_dump($add)
打印 $add
,结果如下:
可以看到它的类型是用于代表匿名函数的 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
:
注意第二个红色方框,我们在运行时将 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;
打印结果都是一样的:
这种在运行时动态设置函数类型值给变量的功能,在 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
,全局变量存在于全局范围,与调用位置无关,所以可以成功引用。
上述代码的执行结果是:
global 的安全隐患
但实际编码中,尽量避免使用 global
关键字,因为一旦声明了全局变量,就可以在任何位置获取到这些全局变量,非常容易导致系统被攻击,比如我们新增一个函数 test
,在这个函数内部就可以试图通过 $GLOBALS
获取对应全局变量:
function test() {
printf("n1 = %d, n2 = %d, n3 = %d\n", $GLOBALS['n1'], $GLOBALS['n2'], $GLOBALS['n3']);
}
匿名函数则有效规避了这种安全隐患。此外,匿名函数的另一个典型应用场景就是兜底处理(fallback),关于这一点,学院君将在作业项目中演示。
2 条评论
在函数里return 闭包函数, 后面除了要调用一次父函数, 还要再加个( ), 好像挺麻烦
好处是可以在运行时真正需要的时候再调用 可以用于延迟加载这种场景