PHP 零基础教程

PHP 匿名函数与箭头函数

匿名函数(Anonymous functions),也称为闭包(closures),是指没有指定名称的函数。你可以将它们赋值给一个变量,然后通过该变量来调用,或者将它们作为参数传递给其他函数。PHP 7.4 引入了箭头函数(Arrow functions),为编写简单的匿名函数提供了更简洁的语法。

这两个核心概念允许我们将函数当作普通值来处理,从而显著提升了代码的复用性与灵活性。本章将带你全面掌握这两种强大特性的用法。

1. 匿名函数(闭包)

匿名函数使用 function 关键字声明,但不需要指定函数名。它们能够捕获其定义时所在作用域中的变量,这就是它们常被称为“闭包”的原因。这种能力使得匿名函数能够“封闭包含”父作用域的变量,让这些变量即使在父作用域执行完毕后,依然可以在匿名函数内部被访问。

1.1 基础语法与使用

定义匿名函数时,使用 function 关键字,后跟可选参数,然后是函数体。你可以将完整的定义赋值给一个变量。

<?php
// 将匿名函数赋值给一个变量
$greet = function($name) {
    return "你好," . $name . "!";
};

// 通过变量调用匿名函数
echo $greet("Alice"); // 输出: 你好,Alice!
echo "\n";

// 将匿名函数作为回调参数传递
function processArray(array $numbers, callable $callback) {
    $results = [];
    foreach ($numbers as $number) {
        $results[] = $callback($number);
    }
    return $results;
}

$numbers = [1, 2, 3, 4, 5];

// 传入一个匿名函数,将每个数字翻倍
$doubledNumbers = processArray($numbers, function($num) {
    return $num * 2;
});

print_r($doubledNumbers);
/* 输出:
Array
(
    [0] => 2
    [1] => 4
    [2] => 6
    [3] => 8
    [4] => 10
)
*/
?>

processArray 示例中,匿名函数被直接作为 $callback 参数传入,这展示了它在高阶函数(接收其他函数作为参数的函数)中的经典用法。

1.2 使用 use 关键字继承变量

匿名函数可以使用 use 关键字从父作用域继承变量。这使得闭包能够访问它被定义时存在的变量,即使在执行闭包时这些变量已经不在当前作用域内。

默认情况下,通过 use 继承的变量是按值传递的。这意味着在父作用域中修改原始变量,并不会影响闭包内部的变量副本。如果你需要按引用传递(继承),必须在变量名前加上取址符号 (&)。

<?php
$multiplier = 2;

$double = function($number) use ($multiplier) {
    return $number * $multiplier;
};

echo $double(5); // 输出: 10
echo "\n";

// 演示 'use' 变量的按值传递
$increment = 10;
$addIncrement = function($number) use ($increment) {
    return $number + $increment;
};

echo $addIncrement(5); // 输出: 15
echo "\n";

$increment = 20; // 改变原始变量的值
echo $addIncrement(5); // 依然输出: 15 (闭包内的 $increment 副本仍然是 10)
echo "\n";

// 演示 'use' 变量的按引用传递
$counter = 0;
$incrementCounter = function() use (&$counter) {
    $counter++;
};

$incrementCounter();
$incrementCounter();
$incrementCounter();

echo $counter; // 输出: 3 (闭包修改了外部的原始变量 $counter)
echo "\n";
?>

当匿名函数需要修改父作用域中的变量,并且希望这些修改在函数外部也能生效时,使用 use (&$variable) 至关重要。

1.3 匿名函数的实际应用场景

匿名函数在需要回调函数(callback)的场景中特别有用,例如事件监听器、数组排序和异步操作。

使用 usort() 对数组进行排序: 当你需要对数组进行自定义逻辑排序时,usort() 接收一个匿名函数作为比较器。

<?php
$products = [
    ['name' => '笔记本电脑', 'price' => 1200],
    ['name' => '鼠标', 'price' => 25],
    ['name' => '键盘', 'price' => 75],
];

// 根据价格对产品进行升序排序
usort($products, function($a, $b) {
    return $a['price'] <=> $b['price']; // <=> 是太空船运算符 (PHP 7+ 引入)
});

print_r($products);
/* 输出:
Array
(
    [0] => Array
        (
            [name] => 鼠标
            [price] => 25
        )
    [1] => Array
        (
            [name] => 键盘
            [price] => 75
        )
    [2] => Array
        (
            [name] => 笔记本电脑
            [price] => 1200
        )
)
*/
?>

使用 array_filter() 过滤数组: 匿名函数可以为你提供简洁明了的过滤条件。

<?php
$numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

// 过滤出偶数
$evenNumbers = array_filter($numbers, function($num) {
    return $num % 2 == 0;
});

print_r($evenNumbers);
/* 输出:
Array
(
    [1] => 2
    [3] => 4
    [5] => 6
    [7] => 8
    [9] => 10
)
*/
?>

2. 箭头函数(短闭包)

PHP 7.4 引入了箭头函数,为编写匿名函数提供了一种更简短的语法,特别适合简单的、只有单行表达式的回调函数。它们拥有隐式作用域,这意味着它们会自动通过按值传递的方式捕获父作用域中的变量。这省去了手动为值传递变量编写 use 关键字的麻烦。

2.1 语法与自动变量捕获

箭头函数使用 fn 关键字定义,后跟参数、一个箭头 => 以及一个单行表达式。这个表达式的计算结果会被隐式返回(不需要写 return 关键字)。

<?php
// $greet 匿名函数的箭头函数等效写法
$greetArrow = fn($name) => "你好," . $name . "!";

echo $greetArrow("Bob"); // 输出: 你好,Bob!
echo "\n";

// 在 processArray 中使用箭头函数
function processArray(array $numbers, callable $callback) {
    $results = [];
    foreach ($numbers as $number) {
        $results[] = $callback($number);
    }
    return $results;
}

$numbers = [10, 20, 30];

// 使用箭头函数将每个数字翻倍
$doubledNumbersArrow = processArray($numbers, fn($num) => $num * 2);

print_r($doubledNumbersArrow);
/* 输出:
Array
(
    [0] => 20
    [1] => 40
    [2] => 60
)
*/
echo "\n";

// 箭头函数会自动通过按值传递捕获父作用域的变量
$factor = 3;
$multiplyByFactor = fn($number) => $number * $factor;

echo $multiplyByFactor(4); // 输出: 12
echo "\n";

$factor = 5; // 改变原始变量
echo $multiplyByFactor(4); // 依然输出: 12 (闭包内 $factor 的副本仍然是 3)
echo "\n";

// 如果要通过引用继承变量,你仍然需要使用带有 'use (&$variable)' 的常规匿名函数
// 箭头函数不支持按引用继承变量。
?>

自动捕获变量的特性让箭头函数在进行简短的数据转换任务时无比方便。但必须牢记:它们是按值捕获的。如果你需要修改外部变量,依然需要使用带有 use (&$variable) 的传统匿名函数。

2.2 箭头函数的实际应用

在需要紧凑的、单表达式回调的场景下,箭头函数大放异彩。

过滤和映射数组(例如:array_map, array_filter

<?php
$items = [
    ['name' => '桌子', 'price' => 150, 'in_stock' => true],
    ['name' => '椅子', 'price' => 50, 'in_stock' => false],
    ['name' => '台灯', 'price' => 30, 'in_stock' => true],
];

// 获取有库存的物品名称
$inStockNames = array_map(fn($item) => $item['name'], array_filter($items, fn($item) => $item['in_stock']));

print_r($inStockNames);
/* 输出:
Array
(
    [0] => 桌子
    [1] => 台灯
)
*/
echo "\n";

// 计算所有物品的总价 (假设每件数量均为 1)
$totalPrice = array_reduce($items, fn($carry, $item) => $carry + $item['price'], 0);
echo "总价: $" . $totalPrice; // 输出: 总价: $230
echo "\n";
?>

事件处理(假设场景)

想象一下你正在构建一个简单的事件调度器。你可以使用箭头函数来定义事件处理器。

<?php
class EventDispatcher {
    private $listeners = [];

    public function on(string $eventName, callable $callback) {
        $this->listeners[$eventName][] = $callback;
    }

    public function dispatch(string $eventName, array $data = []) {
        if (isset($this->listeners[$eventName])) {
            foreach ($this->listeners[$eventName] as $listener) {
                $listener($data);
            }
        }
    }
}

$dispatcher = new EventDispatcher();
$logFile = 'app.log'; // 外部变量,将被按值捕获

// 使用箭头函数作为事件监听器
$dispatcher->on('user.registered', fn($eventData) =>
    file_put_contents($logFile, "用户 '{$eventData['username']}' 于 " . date('Y-m-d H:i:s') . " 注册\n", FILE_APPEND)
);

$dispatcher->dispatch('user.registered', ['username' => 'john.doe', 'email' => 'john@example.com']);

// 这将向 app.log 追加一行类似 "用户 'john.doe' 于 2023-10-27 10:30:00 注册" 的日志
echo "用户注册事件已调度并记录。\n";
?>

在这个例子中,箭头函数 fn($eventData) => ... 充当了一个简洁的事件监听器,它自动捕获了父作用域中的 $logFile 变量。