PHP 开发中,array_map()、array_filter() 和 array_reduce() 经常同时出现在数据处理代码里。三个函数都接受回调函数,看起来只是写法不同,实际承担的任务却完全不同。array_map()主要负责把一组输入元素转换成另一组元素,array_filter()负责根据条件决定哪些元素留下,array_reduce()则负责把整个数组逐步累积成一个结果。理解这三个函数最简单的方法,不是死记函数定义,而是先问自己一个问题:我要修改每个元素、挑选部分元素,还是把所有元素汇总成一个结果?
array_map 负责转换,不负责筛选
如果数组中的每个元素都需要经过相同规则处理,array_map()通常是最直接的选择。例如把用户姓名统一转换成小写:
$names = ['Alice', 'BOB', 'Charlie'];
$result = array_map('strtolower', $names);
执行之后,$result会得到经过转换的新数组。array_map()不会通过回调函数直接修改原数组中的标量值,而是使用回调返回的结果构造输出。
使用闭包处理复杂数据时,可以直接访问数组元素:
$users = [
['name' => 'Alice', 'age' => 30],
['name' => 'Bob', 'age' => 25],
];
$result = array_map(
fn($user) => [
'name' => trim($user['name']),
'age' => (int) $user['age'],
],
$users
);
这里的目标是把一种数据结构转换成另一种数据结构,而不是决定哪些用户应该被删除。
对于对象也一样:
$result = array_map(
fn($user) => $user->name,
$users
);
需要注意的是,如果回调内部修改对象本身的属性,那么修改的是对象实例,而不是 array_map()提供了某种“原地修改数组”的能力。对象属于引用语义范畴,不能把两种行为混为一谈。
array_filter 负责决定哪些元素留下
array_filter()的核心问题是“这个元素是否应该保留”。
例如只保留成年人:
$users = [
['name' => 'Alice', 'age' => 30],
['name' => 'Bob', 'age' => 16],
['name' => 'Carol', 'age' => 25],
];
$result = array_filter(
$users,
fn($user) => $user['age'] >= 18
);
回调返回 true 的元素会被保留,返回 false 的元素会被排除。
这里有一个非常容易被忽略的问题:array_filter()默认会保留原数组的键。
例如:
$numbers = [10, 20, 30, 40];
$result = array_filter(
$numbers,
fn($value) => $value >= 30
);
结果的键仍然可能是 2 和 3,而不是重新变成 0 和 1。
如果后面的代码要求得到连续的数字索引,可以显式调用:
$result = array_values($result);
这在 JSON API 中尤其重要,因为 PHP 数组的键结构会影响 json_encode()最终产生 JSON 对象还是 JSON 数组。
array_filter 不传回调时要特别小心
array_filter()如果省略第二个参数,会删除所有被 PHP 判断为 false 的值。
例如:
$data = [0, 1, '', 'hello', null, false, [], 5];
$result = array_filter($data);
这种行为有时非常方便,但在业务数据处理中也可能产生严重问题。
例如 0可能是一个完全合法的订单数量,'0'可能是合法输入,false也可能是一个有明确业务含义的状态。如果只是想删除 null,就不要把整个 PHP 的 falsey 规则交给 array_filter():
$result = array_filter(
$data,
fn($value) => $value !== null
);
如果只想删除空字符串,也应该明确写出来:
$result = array_filter(
$data,
fn($value) => $value !== ''
);
原稿中把 array_filter('strlen')简单描述成“过滤空字符串”也不够准确。实际使用时还需要考虑回调接收到的值类型。strlen()要求参数适合字符串处理,如果数组中混有 null、整数或其他类型,行为就可能与开发者预期不同。对于业务数据,显式条件往往比依赖类型转换更加可靠。
array_reduce 是聚合,不是普通的数据转换
array_reduce()和前两个函数的区别最大,因为它最终可以把整个数组压缩成一个值。
例如计算订单金额:
$orders = [
['amount' => 120],
['amount' => 80],
['amount' => 250],
];
$total = array_reduce(
$orders,
fn($carry, $order) => $carry + $order['amount'],
0
);
这里的 $carry就是累积状态,$order是当前元素。
计算过程可以理解成:
初始值 0
加入 120
得到 120
加入 80
得到 200
加入 250
得到 450
最终得到的不是一个和原数组等长的新数组,而是一个聚合结果。
因此,看到“把很多元素最终变成一个结果”的需求时,array_reduce()才有明显优势。例如金额汇总、统计数量、构建索引、合并配置、计算复杂状态等。
array_reduce 的初始值不能随便省略
第三个参数是初始累积值。
$total = array_reduce(
$orders,
fn($carry, $order) => $carry + $order['amount'],
0
);
这里使用 0作为初始值,因此即使 $orders是空数组,最终结果也会是 0。
如果没有提供初始值,空数组的结果会是 null。这在业务代码中可能产生类型上的差异。
例如计算平均订单金额时:
$total = array_reduce(
$orders,
fn($carry, $order) => $carry + $order['amount'],
0
);
$count = count($orders);
$average = $count > 0
? $total / $count
: 0;
这里不能只考虑总金额,还必须处理分母为零的情况。
三个函数最大的区别是回调参数
这三个函数最容易写错的地方之一,就是回调函数的参数结构不同。
array_map()处理单个数组时,回调通常接收当前元素:
fn($value) => ...
array_filter()通常也是接收当前元素:
fn($value) => ...
array_reduce()则至少需要处理累积值和当前元素:
fn($carry, $value) => ...
因此下面这种代码的思维方式完全不同:
array_map(
fn($value) => strtoupper($value),
$names
);
这里是“每一个元素都转换”。
而:
array_filter(
$names,
fn($value) => $value !== ''
);
这里是“每一个元素决定留不留”。
而:
array_reduce(
$numbers,
fn($carry, $value) => $carry + $value,
0
);
这里是“当前结果继续吸收下一个元素”。
把这三个概念区分清楚,比记住函数名称更加重要。
三个函数可以组合,但不要为了炫技而组合
实际项目中确实经常需要连续处理。
例如订单数据中先删除取消订单,再提取金额,最后计算总额:
$orders = array_filter(
$orders,
fn($order) => $order['status'] !== 'cancelled'
);
$amounts = array_map(
fn($order) => (float) $order['amount'],
$orders
);
$total = array_reduce(
$amounts,
fn($carry, $amount) => $carry + $amount,
0.0
);
每一步都有明确职责,因此代码比较容易测试和维护。
但如果只是简单地处理几十个元素,为了把所有事情压缩成一条复杂表达式,往往会降低可读性。函数式写法的价值并不是让代码看起来“高级”,而是让数据处理过程具有明确边界。
如果业务逻辑已经复杂到需要大量条件、异常处理、日志记录或者数据库操作,那么普通的 foreach 反而可能更加清楚。
foreach 并没有过时
这是 PHP 数组处理讨论中经常被忽略的一点。
array_map()、array_filter()和array_reduce()并不是 foreach 的全面替代品。
例如需要同时修改多个字段、执行条件判断、记录日志并处理异常:
$result = [];
foreach ($users as $user) {
if (!$user['enabled']) {
continue;
}
$name = trim($user['name']);
if ($name === '') {
continue;
}
$result[] = [
'id' => (int) $user['id'],
'name' => $name,
];
}
如果硬要把这段逻辑拆成多个 array_filter()、array_map(),代码未必更好。
foreach最大的优势是可以在一个明确的控制流中处理多个条件、多个状态以及副作用。对于复杂业务逻辑,它经常比连续套用多个高阶数组函数更容易维护。
性能不能简单理解成哪个函数最快
array_map()、array_filter()和array_reduce()都有 PHP 内部实现,但它们都涉及回调调用。实际性能会受到 PHP 版本、数据规模、回调复杂度、数组类型以及业务代码本身影响。
因此没有必要建立“array_map 一定比 foreach 快”“array_reduce 处理大数组一定很慢”这样的固定结论。
如果数据量只有几百或几千条,通常应该优先考虑代码清晰度。到了几十万甚至更大的数据规模,真正应该首先问的是:这些数据为什么已经全部加载到 PHP 内存里?
如果数据来自 MySQL,而需求只是计算总金额,那么:
SELECT SUM(amount) FROM orders
通常比先把几十万条订单全部查询到 PHP,再通过 array_reduce()逐条计算更加合理。
同样,如果只是过滤数据库中的数据,也应该优先考虑 SQL 的 WHERE 条件,而不是先把所有记录加载到 PHP 再使用 array_filter()。
数组函数解决的是 PHP 内存中的数据处理问题,它们不能替代数据库查询优化。
多数组 array_map 还有额外的语义
array_map()还支持同时传入多个数组:
$a = [1, 2, 3];
$b = [10, 20, 30];
$result = array_map(
fn($x, $y) => $x + $y,
$a,
$b
);
此时回调会同时接收来自多个数组的元素。
这与 array_filter()和 array_reduce()的典型使用方式明显不同。多数组映射适合处理具有位置对应关系的数据,但如果数组长度不同,结果行为就需要根据 PHP 版本和具体调用方式仔细确认,不应该把它简单理解成 SQL 的 JOIN。
PHP 版本和数组键行为也应该纳入考虑
PHP 的数组并不是传统意义上只有数字下标的连续数组,而是有序映射。很多看似简单的数组操作,实际都涉及键是否保留、是否重新编号以及最终 JSON 表示方式。
array_filter()会保留原键,因此过滤之后经常需要:
$result = array_values($result);
array_map()在单数组场景下通常保留输入数组的键结构,但当涉及多个数组时,键处理规则不同。
array_reduce()则完全不同,它最终返回的是累积结果,而不是一个按照输入键继续组织的数组。
对于 API 开发人员来说,这些细节尤其重要,因为 PHP 数组最终经常会进入 json_encode()。一个本来期望得到 JSON 数组的结果,如果键结构没有处理好,可能变成 JSON 对象,从而让前端收到完全不同的数据结构。
最实用的选择方法
实际开发时可以把判断简化成三个问题。
如果问题是“我要把每一个元素变成另一种形式”,优先考虑 array_map()。
如果问题是“我要判断哪些元素应该留下”,优先考虑 array_filter()。
如果问题是“我要把整个数组累计成一个最终结果”,考虑 array_reduce()。
如果三个需求同时存在,就按照业务逻辑拆成多个步骤。先过滤、再转换、再聚合完全可以,但每一步都应该有明确的输入和输出。
如果处理过程中需要复杂控制流、多个状态、异常处理、日志、副作用或者大量条件判断,那么 foreach往往更加合适。
对于数据库数据,还要再往前走一步:如果过滤和聚合本来就可以由数据库完成,就不要为了展示 PHP 数组函数而把大量数据搬到应用层。WHERE、SUM()、COUNT()、GROUP BY以及合适的索引,很多时候比任何 PHP 数组函数都更有效。
PHP 数组函数真正值得掌握的不是三个函数各自的“技巧”,而是数据处理边界。转换、筛选、聚合属于三种不同操作,代码能够清楚表达这三件事情,维护者就不需要猜测作者到底想干什么。至于最终选择 array_map()、array_filter()、array_reduce()还是 foreach,应该由数据结构、业务逻辑、数据规模和执行位置共同决定,而不是为了追求所谓函数式写法而强行套用某个函数。
PHP array_map、array_filter 和 array_reduce 怎么选择?数组处理技巧详解
喜欢这篇报道?
使用下面的功能,方便以后继续阅读和分享 MNewsTV
关于文章收藏
收藏不需要注册帐号,收藏信息仅保存在当前浏览器中。
删除收藏请进入「我的收藏」进行管理。
捐助(Paypal): https://www.paypal.me/observeccp 订阅中国观察电报 Telegram : https://t.me/s/ObserveCCP