前言闭包Closure在 PHP 里就是匿名函数加上它记住定义位置变量的能力。PHP 5.3 引入了匿名函数和Closure类PHP 5.4 让闭包自动绑定$thisPHP 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.4declare(strict_types1);$greet function (string $name): string {return Hello, {$name}!;};echo $greet(Ada), \n; // Hello, Ada!匿名函数所在的文件作用域里的变量不能在函数体内直接访问必须通过use列出来。这是命名函数和匿名函数在作用域上唯一的区别。两种捕获方式的行为差异是本节的重点?php // 适用于 PHP 7.4declare(strict_types1);$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.4declare(strict_types1);final class Order{private string $id A-1001;public function makeChecker(): callable{// 自动绑定 $this无需 usereturn function (): string {return 订单号 . $this-id;};}public function makeStaticChecker(): callable{// 静态闭包没有 $thisreturn 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.0Closure::fromCallable()Closure::fromCallable(callable $callback): ClosurePHP 7.1bindTo()返回的是一个新的闭包原闭包不变失败时返回null——这个返回值必须检查否则后续调用会得到Call to a member function on null这类难懂的报错。第三个参数$newScope决定了新闭包能访问哪个类的非 public 成员测试框架和容器常用它来构造替身。?php // 适用于 PHP 7.0declare(strict_types1);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.0declare(strict_types1);$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 起固定返回true8.0 之前返回布尔值所以不要依赖它的返回值判断排序是否成功。array_filter()会保留原键名过滤后的结果键名不连续需要连续索引时要补array_values()。2. 构造配置好的函数。把一组参数固定在闭包里得到一个更专用的可调用对象?php // 适用于 PHP 7.4declare(strict_types1);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.4declare(strict_types1);final class Pipeline{/** var arrayint, 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 () {}需要显式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 都出在第一条上因为它写错时不报错只是结果不对。