news 2026/10/10 7:05:15

PHP遍历数组的几种方法

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
PHP遍历数组的几种方法

前言


遍历数组是 PHP 里最日常的操作,foreach几乎成了条件反射。但foreach有两个截然不同的版本——按值遍历和按引用遍历,它们对「循环里修改数组」这件事的反应完全不同,性能特征也不一样。很多莫名其妙的 bug 就出在这里:循环体内改了一个元素,结果要么完全没生效,要么影响了后续的遍历。


第二类误解是把array_map()、array_walk()、array_filter()当成「foreach的简写」。它们看起来都是「对每个元素做点什么」,但返回值规则、键的处理、能否修改原数组,三者各不相同。用错了写出来的代码能跑,只是结果不是你想要的。


还有一段历史必须点明:早期 PHP 教程里常见的while (list($k, $v) = each($arr))这种写法,each()函数已在 PHP 8.0 中被彻底移除,不是废弃而是移除,网上很多老代码直接跑不起来。与之类似的还有mysql_*系列(PHP 7.0 移除)、ereg_*(PHP 7.0 移除)、split()(PHP 7.0 移除),这些都必须换成现行写法。


本文按「按值遍历」「按引用遍历」「函数式遍历」「手动指针遍历」四条线讲清遍历的机制与取舍,示例以 PHP 8.0 及以上为基准。


一、foreach 按值遍历:遍历的是快照


foreach按值遍历时,循环变量拿到的是元素的副本。PHP 7 起,foreach不再使用数组的内部指针,而是对数组做一份逻辑上的快照(物理上仍靠写时复制延迟复制)。这带来两个可预期的结论:


<?php // 适用于 PHP 8.0+

$nums = [1, 2, 3];

// 修改 $v 不影响原数组
foreach ($nums as $v) {
$v = $v * 10;
}
print_r($nums); // [1, 2, 3] —— 原数组没变

// 循环里往原数组追加元素,不会延长循环
foreach ($nums as $v) {
if ($v === 1) {
$nums[] = 100; // 追加发生在循环内部
}
echo $v, ' ';
}
echo PHP_EOL; // 1 2 3 —— 只遍历了最初的三个元素

第二条是「快照」语义最直观的证据:按值遍历时,循环的次数在开始时就已经确定,中途往数组里塞东西不会让循环变长。这既是安全网(不会写出死循环),也是一个陷阱——如果你指望「边遍历边追加、边处理新元素」,foreach做不到,得改用while加array_shift(),或者用生成器(generator)。


嵌套遍历里用同一个变量名也要小心,内层会覆盖外层的循环变量:


<?php // 适用于 PHP 8.0+

$groups = [['a', 'b'], ['c']];

// ❌ 内外层都用 $v,内层结束后外层 $v 已被覆盖(这里外层直接结束所以看不出问题)
foreach ($groups as $v) {
foreach ($v as $v) {
echo $v, ' ';
}
}

// ✅ 内层换个名字,可读性也更好
foreach ($groups as $group) {
foreach ($group as $item) {
echo $item, ' ';
}
}

二、foreach 按引用遍历:直接改原数组


如果目的是修改原数组,用引用遍历foreach ($arr as &$v)。此时循环变量是元素的别名,赋值直接落到原数组上:


<?php // 适用于 PHP 8.0+

$prices = [100, 200, 300];

foreach ($prices as &$p) {
$p = (int) round($p * 1.1); // 直接改原数组
}
unset($p); // ⚠️ 必须:断开引用

print_r($prices); // [110, 220, 330]

unset($p)这一行不是可选项。循环结束后$p仍然指向数组的最后一个元素,如果你马上再用同名变量做按值遍历,就会把最后一个元素改掉:


<?php // 适用于 PHP 8.0+

$arr = [1, 2, 3];

foreach ($arr as &$v) { } // 忘记 unset($v)
foreach ($arr as $v) { } // $v 已经绑定到 $arr[2]

print_r($arr); // [1, 2, 2] —— 最后一个元素被改成了它自己

数组元素本身是数组或对象时,按值遍历也能改到「里面」:


<?php // 适用于 PHP 8.0+

$rows = [['n' => 1], ['n' => 2]];

foreach ($rows as $row) {
$row['n'] = 99; // ❌ 只改了副本,$rows 不变
}

foreach ($rows as $i => $row) {
$rows[$i]['n'] = 99; // ✅ 按下标回写,改到原数组
}

对象则不同,因为对象变量保存的是句柄,按值遍历时副本仍指向同一个对象,改属性会改到原对象。这个差异在「数组里放对象」时极易混淆,判断标准很简单:值是标量或数组,副本独立;值是对象,副本共享。


在按引用遍历的循环里修改数组结构(增删元素)是不可靠的。官方手册对此的表述是这种行为不保证可预期,因此不要写「边引用遍历边unset()或追加」的代码。需要过滤就在循环外收集待删键,循环结束后统一处理:


<?php // 适用于 PHP 8.0+

$data = ['a' => 1, 'b' => null, 'c' => 3];

// ✅ 先收集,循环结束后再删,避免遍历中改结构
$toRemove = [];
foreach ($data as $k => $v) {
if ($v === null) {
$toRemove[] = $k;
}
}
foreach ($toRemove as $k) {
unset($data[$k]);
}
// 不过这个场景其实用 array_filter() 一步就够了

三、函数式遍历:map / walk / filter / reduce 的分工


这四个函数经常被混用,它们的分工其实很规整:




函数返回值键的处理能否改原数组用途



array_map()新数组只传一个数组时保留键,传多个数组时键被重新索引否一对一映射

array_walk()布尔值(成功与否)原位处理能(回调首参声明为引用时)就地修改

array_filter()新数组(保留键)保留原键否按条件筛选

array_reduce()单个值不适用否累积成一个结果

array_merge()之类新数组见各自规则否合并



<?php // 适用于 PHP 8.0+

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

// array_map:一对一映射,返回新数组
$squares = array_map(fn(int $n): int => $n * $n, $nums);
print_r($squares); // [1, 4, 9, 16, 25]

// array_walk:就地修改,回调第一个参数必须是引用
array_walk($nums, function (int &$n, int $i): void {
$n = $n * 2;
});
print_r($nums); // [2, 4, 6, 8, 10]

// array_filter:筛选,注意保留原键!
$even = array_filter($nums, fn(int $n): bool => $n % 4 === 0);
print_r($even); // [1 => 4, 3 => 8] —— 键是 1 和 3,不是 0 和 1

// 要连续键就再补一次 array_values()
print_r(array_values($even)); // [4, 8]

// array_reduce:累积
$sum = array_reduce($nums, fn(int $carry, int $n): int => $carry + $n, 0);
echo $sum, PHP_EOL; // 30

三个关键差异记牢:array_map()的回调不能通过引用改原数组(改的是副本);array_walk()的回调第一个参数不声明成&就改不到原数组;array_filter()会保留原键,结果不是「干净的列表」,直接json_encode()会变成 JSON 对象。


还有一个老读者熟悉的陷阱:array_map()在只传一个数组时保留键,传两个及以上数组时键会被重新编号(因为它做的事情是「拉链式并排」)。


四、手动指针与生成器


foreach之外,PHP 还保留了操作数组内部指针的函数:reset()、current()、key()、next()、prev()、end()。它们对同一数组共享同一个内部指针,因此在foreach内部调用会互相干扰——foreach在 PHP 7 起不再依赖内部指针,但这些函数自己仍是共享状态的。


<?php // 适用于 PHP 8.0+

$q = ['first', 'second', 'third'];

reset($q);
while (($k = key($q)) !== null) {
echo $k, ' => ', current($q), PHP_EOL;
next($q);
}
// 0 => first
// 1 => second
// 2 => third

遍历完指针停在末尾,想再走一遍必须先reset()。这套写法在现代 PHP 里主要用于需要「中途停下、下次继续」的场景。


PHP 8.0 移除了each(),下面这段在 PHP 5 里到处都是的代码现在会直接报Call to undefined function each():


<?php // ❌ 已失效:each() 在 PHP 8.0 被移除,split()/ereg_* 在 PHP 7.0 被移除

// while (list($k, $v) = each($arr)) {
// echo "$k => $v\n";
// }

现行写法就是foreach,list()本身还在(PHP 7.1 起支持短语法方括号解构):


<?php // 适用于 PHP 8.0+

$arr = ['a' => 1, 'b' => 2];
foreach ($arr as $k => $v) {
echo $k, ' => ', $v, PHP_EOL;
}

数据量很大、或者元素需要「边算边取」时,用生成器(generator,PHP 5.5 起)避免一次性构造整个数组:


<?php // 适用于 PHP 8.0+

function readLines(string $path): Generator {
$fh = fopen($path, 'r');
if ($fh === false) {
return;
}
try {
while (($line = fgets($fh)) !== false) {
yield rtrim($line, "\r\n");
}
} finally {
fclose($fh);
}
}

// foreach 可以直接遍历生成器,内存里不会同时存在所有行
foreach (readLines(__DIR__ . '/data.txt') as $line) {
echo $line, PHP_EOL;
}

生成器是一次性的:遍历完(或中途break)之后不能重来,需要重新调用函数生成一个新的。另外生成器里的return;表示结束,return $value取到的值只能通过getReturn()拿到,不会出现在foreach的元素里。


常见坑点



  1. 按引用遍历后忘记unset()


❌foreach ($a as &$v) {}紧接着foreach ($a as $v) {},最后一个元素被改写成它自己的值 ✅ 引用循环结束立刻unset($v);



  1. 用array_map()的回调去改原数组


❌array_map(function ($x) { $x++; }, $arr);以为改了$arr—— 回调拿到的是副本,且返回值被丢弃 ✅ 要改原数组用array_walk($arr, function (&$x) { $x++; }),或者接收array_map()的返回值



  1. array_walk()回调参数忘了写引用


❌array_walk($a, function ($v) { $v = strtoupper($v); });结果原数组一点没变 ✅ 第一个参数声明为&$v:function (&$v) { $v = strtoupper($v); }



  1. array_filter()之后直接交给前端


❌json_encode(array_filter($list))输出的是 JSON 对象,因为键 1、3 被保留下来 ✅ 需要列表就再套一层array_values()



  1. 在引用遍历里增删元素


❌foreach ($a as $k => &$v) { unset($a[$k]); }—— 遍历中改结构,结果不保证 ✅ 循环里只做标记,循环后用array_filter()或统一的删除步骤处理



  1. 还在用each()或split()


❌while (list($k, $v) = each($arr))——each()在 PHP 8.0 被移除;split()在 PHP 7.0 被移除(改用explode()/preg_split()) ✅ 直接改用foreach ($arr as $k => $v)



  1. 以为count()写在for条件里很慢、必须提到循环外


❌ 用「count()每轮都数一遍,提到外面性能翻倍」来解释,属于想当然 ✅ PHP 数组是哈希表,结构里存着元素个数,count()是 O(1);要不要提出去主要看可读性(循环里会改数组长度时就别提)



  1. 混用foreach和current()/next()


❌ 在foreach循环体里调next($arr),指望推进循环 ✅foreach自己控制进度,指针函数只用于while手写遍历;两者不要混用


总结




遍历方式循环变量是副本还是别名循环中追加元素的影响典型用途



foreach ($a as $v)副本(标量、数组)不影响遍历次数只读遍历

foreach ($a as &$v)别名行为不保证,应避免就地修改元素

foreach ($a as $k => $v)副本同上需要键时

array_map()副本不适用生成新数组

array_walk()别名(参数加&时)不适用就地修改

array_filter()副本不适用筛选(键会保留)

while+next()内部指针会受影响可中断的遍历

foreach+ 生成器一次一个不适用大数据流式处理



选遍历方式时先问自己两个问题:我要不要改原数组,我要不要一个新数组。要改原数组就用引用或array_walk(),要新数组就用array_map()/array_filter(),两者都不要就老实用foreach。至于each()这类老写法,记住它们在 PHP 7.0/8.0 已经被移除,不要再从旧代码里抄。




版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/10 7:04:29

微信wxid转二维码工具包:原理、PHP实现与排错指南

简介&#xff1a;一套轻量级前端工具包&#xff0c;可将微信用户wxid&#xff08;如wxid_xxxxxx&#xff09;快速转换成可扫码添加的好友链接二维码&#xff0c;适用于已知对方wxid但未保存聊天、误删好友后想快速重新添加的场景。整套方案完全在本地浏览器运行&#xff0c;无需…

作者头像 李华
网站建设 2026/10/10 7:03:07

统信UOS下HP打印机安装失败的三大核心适配点

1. 为什么统信UOS装HP打印机总卡在“找不到设备”或“打印测试页失败”统信UOS系统装HP打印机&#xff0c;表面看只是点几下鼠标、选个驱动、按个确认——但实际操作中&#xff0c;90%以上的问题根本不是“不会点”&#xff0c;而是系统底层对打印任务的调度逻辑被悄悄绕过了。…

作者头像 李华
网站建设 2026/10/10 7:01:34

PostgreSQL内存调优:shared_buffers与work_mem的协同配置

1. 两个参数各管什么&#xff1a;先明确职责边界做数据库运维的人&#xff0c;迟早要面对这两个参数。刚接触PostgreSQL时&#xff0c;我干过一件蠢事&#xff1a;把shared_buffers从128MB调到8GB&#xff0c;满心期待查询能“飞”起来&#xff0c;结果系统整台机器swap飙升&am…

作者头像 李华
网站建设 2026/10/10 7:00:53

JWT Claims设计避坑指南:从Payload字段到校验实战

JWT几乎成了现代后端服务的标配&#xff0c;但凡涉及用户登录、接口鉴权&#xff0c;大家都会顺手甩一个Token出来。但很多开发者用了一年半载JWT&#xff0c;对Header、Payload、Signature三段式结构还是有点懵。尤其是Payload里的Claims&#xff0c;看着就是一堆键值对&#…

作者头像 李华
网站建设 2026/10/10 6:57:58

FISCO BCOS+Spring Boot+Vue区块链电商存证实战

简介&#xff1a;这是一份面向 Java 全栈初学者的 FISCO BCOS 区块链电商项目入门文档&#xff0c;将 Spring Boot 与 Vue 前后端分离架构和联盟链应用结合起来&#xff0c;适合想了解区块链环境搭建、智能合约部署及链上业务集成的开发者。资源为 1 个 PDF 文件&#xff0c;大…

作者头像 李华
网站建设 2026/10/10 6:57:55

服务器硬件全解析:从CPU选型、内存ECC到带外管理

1. 服务器硬件与传统 PC 的本质区别做服务器硬件选型这么多年&#xff0c;我越发觉得一个观点有必要先说清楚&#xff1a;服务器不是“更贵的 PC”&#xff0c;而是“为持续输出算力而设计的专用设备”。普通 PC 关机可以重启&#xff0c;但服务器要求的是 724 小时的稳定运行&…

作者头像 李华