应使用 array_replace_recursive() 实现递归覆盖合并,它对同键标量值和数组均执行后者覆盖前者;而 array_merge_recursive() 会将同键值转为数组,不符合覆盖预期。

PHP array_merge_recursive() 会把同键值变成数组,不是你想要的“覆盖式合并”
直接用 array_merge_recursive() 很容易踩坑:它遇到相同字符串键时,不覆盖而是把值塞进一个新数组。比如 ['name' => 'a'] 和 ['name' => 'b'] 合并后变成 ['name' => ['a', 'b']],而不是 ['name' => 'b']。这在配置覆盖、API响应补全等场景完全不符合预期。
真正需要的是“递归覆盖合并”——子数组继续递归,标量值/数组被后者覆盖(除非你明确想保留前者)。
- 用
array_replace_recursive()是最简方案(PHP 5.3+),它默认就是覆盖语义 - 注意它只处理关联数组;若输入含数字键,行为等价于
array_replace()(即重索引,不递归) - 如果源数组里有
null或空数组,它们会原样覆盖目标对应位置,不会跳过
手动写递归合并函数时,必须判断类型再分支处理
自定义函数更可控,但容易漏掉边界情况。核心逻辑是:对每个键,先判断目标和来源的值类型,再决定是覆盖、递归还是跳过。
典型错误是没区分 is_array() 和 is_array($src[$key]),导致非数组值被误当数组递归,抛出警告。
立即学习“PHP免费学习笔记(深入)”;
- 只对「双方都是数组」才递归调用自身;否则直接用源值覆盖目标值
- 目标数组不存在该键时,不能跳过,要主动赋值(
$target[$key] = $source[$key]) - 如果希望空数组不覆盖已有值,得额外加条件:
!empty($source[$key]) || !is_array($source[$key])
function array_merge_recursive_overwrite($target, $source) {
foreach ($source as $k => $v) {
if (is_array($v) && isset($target[$k]) && is_array($target[$k])) {
$target[$k] = array_merge_recursive_overwrite($target[$k], $v);
} else {
$target[$k] = $v;
}
}
return $target;
}
多维合并时要注意引用与拷贝,避免意外修改原数组
PHP 数组默认传值,但如果你在函数里直接修改 $target 参数(比如用 &$target 声明引用),后续逻辑可能受干扰。尤其在循环中反复合并多个配置数组时,原始模板被改了就很难调试。
- 不要用引用参数,老老实实
return新数组 - 如果性能敏感且数组极大,可考虑用
array_walk_recursive()配合闭包收集路径,但实现复杂度陡增,一般没必要 - 第三方库如
symfony/polyfill-php80提供了array_merge_recursive_distinct(),语义更清晰,但引入依赖需权衡
合并结果里出现 __proto__ 或 constructor 键?那是前端传来的 JSON 被 PHP 自动转义了
这不是合并逻辑的问题,而是数据源头混入了 JS 特有属性。PHP 不会主动过滤这些键名,合并后照样保留。如果下游是给 JS 用的 JSON 接口,这些键可能引发安全或兼容问题。
- 在合并前用
array_filter()清洗键名:array_filter($arr, function($k) { return !in_array($k, ['__proto__', 'constructor'], true); }, ARRAY_FILTER_USE_KEY) - 更稳妥的做法是在接收请求时就做白名单校验,而不是等到合并阶段补救
- 别指望
json_encode()自动忽略它们——只要 PHP 数组里存在,就会被输出
array_replace_recursive() 就够了;剩下 10% 才需要自定义逻辑。关键不是写得多漂亮,而是想清楚:你要覆盖,还是追加?要不要过滤键?原数组能不能动?这几个问题没答案,代码写得再工整也容易返工。



















