☰
PHP闭包(Closure)使用详解
2026/10/10 3:53:32 网站建设 项目流程

前言


闭包(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): mixedPHP 7.0

Closure::fromCallable()Closure::fromCallable(callable $callback): ClosurePHP 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-world

4. 递归闭包。闭包定义时还没有变量名可以引用自己,需要靠引用捕获把变量"接进去":


<?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。


常见坑点



  1. ❌ 以为use ($x)是引用,闭包外改动$x后期待闭包读到新值;✅ 默认是按值快照,需要跟随变化必须写use (&$x)。

  2. ❌ 循环foreach ($arr as &$v)结束后忘了unset($v),之后再写一个同名变量的foreach会莫名改写数组最后一个元素;✅ 引用遍历结束后立刻unset($v)。

  3. ❌ 以为箭头函数fn() => $x能读到$x的后续变化;✅ 箭头函数同样是按值捕获,只是省了use;要引用语义必须退回function () use (&$x)。

  4. ❌ 把闭包serialize()后写进 session、缓存或队列;✅Closure不可序列化,会抛Exception: Serialization of 'Closure' is not allowed,应改存可重建的标识,或在目标端重新构造。

  5. ❌ 直接调用$closure->bindTo($obj)的返回值而不判空;✅ 绑定失败返回null,先判断再调用。

  6. ❌ 在static function或bindTo(null)之后的闭包里用$this;✅ 静态闭包不绑定实例,$this会抛Error: Using $this when not in object context。

  7. ❌ 以为闭包捕获对象属性是"拷贝一份",于是放心地在闭包里改$this->items;✅ PHP 对象是句柄语义,闭包内外拿到的是同一个对象,改动互相可见,反之亦然。

  8. ❌ 把闭包长期挂在事件监听器、路由表这类长生命周期的容器里;✅ 闭包持有$this会延长对象存活时间,用完应及时移除监听,或改成static闭包、显式传参的形式。


总结




特性语法捕获规则版本要求



匿名函数function () {}需要显式usePHP 5.3+

按值捕获use ($x)创建时快照PHP 5.3+

引用捕获use (&$x)实时读写外部变量PHP 5.3+

箭头函数fn() => expr自动按值捕获,不可写usePHP 7.4+

静态闭包static function () {}不绑定$thisPHP 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 都出在第一条上,因为它写错时不报错,只是结果不对。




需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询