use 关键字捕获变量的两种绑定方式(值 vs 引用)
解读
在 PHP 的闭包(匿名函数)中,use 用来把父作用域变量“捕获”到函数体内。
国内主流版本(5.6+,生产环境以 7.4/8.x 为主)默认按值捕获,显式加 “&” 则按引用捕获。
面试时,考官往往先让你口头解释,再补一段代码:让你预测输出、找 bug、或者重构性能。
核心考点:
- 两种绑定发生的时机(定义时 vs 调用时)。
- 对循环内创建闭包、异步回调(Swoole、WorkerMan)、以及 GC 的影响。
- 与对象默认“引用”语义的区别,防止“值捕获”误以为会同步外部状态。
知识点
- 值捕获(默认):
- 在闭包定义瞬间完成变量拷贝,之后外部变量如何变化,闭包内部不受影响。
- 对对象变量而言,拷贝的是“句柄值”,对象内部状态仍同步,但重新赋值外部变量不会同步到闭包。
- 引用捕获(&$var):
- 闭包内部与外部共享同一 zval,任何一方写操作立即对另一方可见。
- 在 foreach 循环中如果批量生成闭包,必须用 & 才能保证所有闭包看到最终值,否则只能看到最后一次拷贝。
- 语法限制:
- use 列表里不能是表达式,只能写变量名;PHP 8 起允许省略捕获而用短闭包 fn() =>,但短闭包自动值捕获,不支持引用。
- 性能与内存:
- 值捕获会额外复制 zval,若拷贝大数组或大对象,内存峰值上涨;引用捕获几乎零拷贝,但要承担“副作用”风险。
- 工程规范:
- PSR-12 未强制,但国内大厂代码审查普遍要求:若闭包生命周期超出当前函数(回调注册、队列任务),必须注释说明为何用引用,避免后续维护者踩坑。
答案
示例代码演示两种绑定差异,可直接写在白板上:
<?php
// 值捕获
$v = 10;
$f1 = function () use ($v) {
echo $v, PHP_EOL; // 10
};
$v = 20;
$f1(); // 输出 10,不受外部后续赋值影响
// 引用捕获
$v = 10;
$f2 = function () use (&$v) {
echo $v, PHP_EOL; // 10
};
$v = 20;
$f2(); // 输出 20,共享同一变量
// 对象句柄的“值捕获”陷阱
$obj = new stdClass();
$obj->num = 1;
$f3 = function () use ($obj) {
$obj->num++; // 外部对象状态会改
};
$obj->num = 5;
$f3();
echo $obj->num, PHP_EOL; // 6,说明拷贝的是句柄,不是对象本体
// 重新赋值对象变量
$obj = new stdClass(); // 外部指向新实例
$obj->num = 99;
$f3(); // 仍操作旧实例,输出 7,进一步证明“值捕获”不会同步变量绑定
?>
口头总结:
“use 默认按值捕获,定义时快照;加 & 后按引用捕获,与外部同步。对象变量默认拷贝句柄,因此内部属性共享,但重新赋值外部变量不会同步到闭包。”
拓展思考
- 循环生成闭包的经典坑:
若批量注册路由、定时任务,foreach 里 use($i) 会得到全部闭包都引用最后一次循环值;解决方式一是用 &,二是用额外参数默认值强行快照。 - Swoole/FPM 差异:
在 Swoole 常驻进程内,引用捕获的变量如果跨协程修改,会出现内存可见性问题,需配合 Channel 或 Context 库管理状态。 - 短闭包 fn() => 的局限性:
PHP 8 提供的短闭包只能值捕获,且不能换行写复杂逻辑;在性能敏感路径(如 array_map 回调)可优先使用,减少 use 列表样板代码。 - 与 JavaScript 闭包对比:
JS 默认按引用捕获,PHP 默认按值;前端转全栈的候选人常在这里翻车,面试时可主动对比,展示跨语言理解深度。