前言
闭包(Closure)在 PHP 里就是匿名函数加上它"记住"定义位置变量的能力。PHP 5.3 引入了匿名函数和Closure类,PHP 5.4 让闭包自动绑定$this,PHP 7.4 加了箭头函数fn(),PHP 8.1 又加了一等可调用语法strlen(...)。这几步演化让"把函数当数据传"从别扭变成了顺手,但每一代写法在捕获变量的规则上都不完全一样,混着用很容易出问题。
三个最常见的误解先说清楚。
第一,以为use ($x)是引用传递。恰恰相反,默认是按值捕获,而且是在闭包被创建的那一刻做一次快照,不是调用时再去取值。想让它跟随外部变化,必须显式写use (&$x)。
第二,以为闭包可以序列化。Closure实例不能被serialize(),会直接抛出异常。想把"一段逻辑"塞进 session 或者缓存里,靠序列化是做不到的。
第三,以为new Closure()可以手工创建。Closure类的构造函数不可用,尝试实例化会得到Error: Instantiation of 'Closure' is not allowed。你只能通过匿名函数、箭头函数、Closure::fromCallable()或一等可调用语法得到闭包对象。
本文按照"捕获规则 → 绑定规则 → 实战用法"的顺序讲,重点是那些写错了也不报错、只是结果不对的地方。
一、基本形式与 use 的两种捕获方式
最基础的形式是把匿名函数赋给变量,再当普通函数调用:
<?php // 适用于 PHP 7.4+
declare(strict_types=1);
$greet = function (string $name): string {
return "Hello, {$name}!";
};
echo $greet('Ada'), "\n"; // Hello, Ada!匿名函数所在的文件作用域里的变量,不能在函数体内直接访问,必须通过use列出来。这是命名函数和匿名函数在作用域上唯一的区别。两种捕获方式的行为差异是本节的重点:
<?php // 适用于 PHP 7.4+
declare(strict_types=1);
$n = 10;
$byValue = function () use ($n): int { return $n; };
$byRef = function () use (&$n): int { return $n; };
$n = 20;
echo $byValue(), "\n"; // 10:捕获发生在闭包创建时
echo $byRef(), "\n"; // 20:引用,实时读取按值捕获值得再强调一次:快照的时机是闭包被创建的那一刻。上面$byValue定义时$n是 10,之后把$n改成 20 也不会影响它。而$byRef捕获的是变量的引用,读到的永远是最新值。
引用捕获的典型用途是让闭包能修改外部变量,比如做一个计数器:
<?php // 适用于 PHP 7.4+
$count = 0;
$inc = function () use (&$count): void { $count++; };
$inc();
$inc();
$inc();
echo $count, "\n"; // 3如果把&去掉,$count永远是 0——这是新手最常遇到的"闭包里改了变量外面没变"的原因。
PHP 7.4 引入的箭头函数(arrow function)是use的语法糖:它自动按值捕获所有用到的外层变量,不需要也不能写use,函数体只能是单个表达式。
<?php // 需要 PHP 7.4 及以上
$factor = 3;
$times = fn(int $x): int => $x * $factor;
echo $times(5), "\n"; // 15箭头函数只做按值捕获,想要引用语义就必须退回function () use (&$x)。
二、$this 绑定与对象作用域
在类的方法里定义闭包时,$this的处理分几个阶段:
| PHP 版本 | 类方法内匿名函数的$this | 说明 |
|---|
| 5.3 | 不可用 | 传use ($this)是语法错误 |
| 5.4 起 | 自动绑定到所在对象 | 无需use,直接用 |
| 5.4 起 | 声明为static function ()则不绑定 | 静态闭包 |
也就是说,下面这种写法在 PHP 8 里是成立的:
<?php // 适用于 PHP 7.4+
declare(strict_types=1);
final class Order
{
private string $id = 'A-1001';
public function makeChecker(): callable
{
// 自动绑定 $this,无需 use
return function (): string {
return '订单号:' . $this->id;
};
}
public function makeStaticChecker(): callable
{
// 静态闭包:没有 $this
return static function (): string {
return '不依赖实例状态';
};
}
}
$checker = (new Order())->makeChecker();
echo $checker(), "\n"; // 订单号:A-1001如果写成static function却用了$this,运行时会抛Error: Using $this when not in object context。
需要把闭包"挪到"另一个对象上执行时,用Closure::bind()或者它的实例方法形式bindTo():
| 方法 | 签名 | 引入版本 |
|---|
Closure::bind() | `Closure::bind(Closure $closure, ?object $newThis, object\ | string\ |
Closure::bindTo() | `$closure->bindTo(?object $newThis, object\ | string\ |
Closure::call() | $closure->call(object $newThis, mixed ...$args): mixed | PHP 7.0 |
Closure::fromCallable() | Closure::fromCallable(callable $callback): Closure | PHP 7.1 |
bindTo()返回的是一个新的闭包(原闭包不变),失败时返回null——这个返回值必须检查,否则后续调用会得到"Call to a member function on null"这类难懂的报错。第三个参数$newScope决定了新闭包能访问哪个类的非 public 成员,测试框架和容器常用它来构造替身。
<?php // 适用于 PHP 7.0+
declare(strict_types=1);
final class Counter
{
private int $value = 5;
}
$getter = function (): int {
return $this->value; // 这里的 $this 由绑定时决定
};
$bound = $getter->bindTo(new Counter(), Counter::class);
if ($bound !== null) {
echo $bound(), "\n"; // 5
}注意第三个参数传的是Counter::class而不是字符串字面量,这样重构时不会漏改。
三、实战:闭包的几种典型用法
1. 传给数组函数当回调。这是最日常的用法,配合usort()、array_filter()、array_map()使用:
<?php // 适用于 PHP 8.0+
declare(strict_types=1);
$items = [
['name' => 'pen', 'price' => 3],
['name' => 'book', 'price' => 25],
['name' => 'bag', 'price' => 12],
];
usort($items, fn(array $a, array $b): int => $a['price'] <=> $b['price']);
print_r($items);
$expensive = array_filter($items, fn(array $i): bool => $i['price'] >= 10);
print_r($expensive);这里有两个细节值得留意。usort()的比较回调必须返回整数,用<=>太空船操作符最省事;另外usort()在 PHP 8.0 起固定返回true,8.0 之前返回布尔值,所以不要依赖它的返回值判断排序是否成功。array_filter()会保留原键名,过滤后的结果键名不连续,需要连续索引时要补array_values()。
2. 构造"配置好的函数"。把一组参数固定在闭包里,得到一个更专用的可调用对象:
<?php // 适用于 PHP 7.4+
declare(strict_types=1);
function makeTruncator(int $limit): callable
{
return function (string $text) use ($limit): string {
if (mb_strlen($text, 'UTF-8') <= $limit) {
return $text;
}
return mb_substr($text, 0, $limit, 'UTF-8') . '...';
};
}
$short = makeTruncator(5);
echo $short('Hello, world'), "\n"; // Hello...$limit被封装进了闭包,调用方不需要每次传——这正是"闭包记住定义环境"的价值。
3. 管道与中间件。把多个闭包串起来依次处理数据,是框架里非常常见的模式:
<?php // 适用于 PHP 7.4+
declare(strict_types=1);
final class Pipeline
{
/** @var array<int, callable> */
private array $stages = [];
public function pipe(callable $stage): self
{
$this->stages[] = $stage;
return $this;
}
public function process(string $payload): string
{
foreach ($this->stages as $stage) {
$payload = (string) $stage($payload);
}
return $payload;
}
}
$pipeline = (new Pipeline())
->pipe(fn(string $s): string => trim($s))
->pipe(fn(string $s): string => strtolower($s))
->pipe(fn(string $s): string => str_replace(' ', '-', $s));
echo $pipeline->process(' Hello PHP World '), "\n"; // hello-php-world4. 递归闭包。闭包定义时还没有变量名可以引用自己,需要靠引用捕获把变量"接进去":
<?php // 适用于 PHP 7.4+
$factorial = function (int $n) use (&$factorial): int {
return $n <= 1 ? 1 : $n * $factorial($n - 1);
};
echo $factorial(5), "\n"; // 120这里的&$factorial是必需的:按值捕获的话,捕获发生时$factorial还是null。
常见坑点
- ❌ 以为
use ($x)是引用,闭包外改动$x后期待闭包读到新值;✅ 默认是按值快照,需要跟随变化必须写use (&$x)。 - ❌ 循环
foreach ($arr as &$v)结束后忘了unset($v),之后再写一个同名变量的foreach会莫名改写数组最后一个元素;✅ 引用遍历结束后立刻unset($v)。 - ❌ 以为箭头函数
fn() => $x能读到$x的后续变化;✅ 箭头函数同样是按值捕获,只是省了use;要引用语义必须退回function () use (&$x)。 - ❌ 把闭包
serialize()后写进 session、缓存或队列;✅Closure不可序列化,会抛Exception: Serialization of 'Closure' is not allowed,应改存可重建的标识,或在目标端重新构造。 - ❌ 直接调用
$closure->bindTo($obj)的返回值而不判空;✅ 绑定失败返回null,先判断再调用。 - ❌ 在
static function或bindTo(null)之后的闭包里用$this;✅ 静态闭包不绑定实例,$this会抛Error: Using $this when not in object context。 - ❌ 以为闭包捕获对象属性是"拷贝一份",于是放心地在闭包里改
$this->items;✅ PHP 对象是句柄语义,闭包内外拿到的是同一个对象,改动互相可见,反之亦然。 - ❌ 把闭包长期挂在事件监听器、路由表这类长生命周期的容器里;✅ 闭包持有
$this会延长对象存活时间,用完应及时移除监听,或改成static闭包、显式传参的形式。
总结
| 特性 | 语法 | 捕获规则 | 版本要求 |
|---|
| 匿名函数 | function () {} | 需要显式use | PHP 5.3+ |
| 按值捕获 | use ($x) | 创建时快照 | PHP 5.3+ |
| 引用捕获 | use (&$x) | 实时读写外部变量 | PHP 5.3+ |
| 箭头函数 | fn() => expr | 自动按值捕获,不可写use | PHP 7.4+ |
| 静态闭包 | static function () {} | 不绑定$this | PHP 5.4+ |
| 绑定到对象 | bindTo()/bind() | 改变$this与作用域 | PHP 5.4+ |
| 绑定并调用 | Closure::call() | 等价于绑定后立即调用 | PHP 7.0+ |
| 任意可调用转闭包 | Closure::fromCallable() | —— | PHP 7.1+ |
| 一等可调用 | strlen(...) | 由函数或方法生成闭包 | PHP 8.1+ |
记住三条主线的区别就不会出大错:捕获是值还是引用(use ($x)与use (&$x))、绑定的是哪个对象($this与静态闭包)、生命周期归谁管(谁能释放闭包持有的对象)。绝大多数闭包相关的 bug 都出在第一条上,因为它写错时不报错,只是结果不对。