php对象克隆默认为浅拷贝,即仅复制属性值,引用类型(如对象、数组)仍共享同一内存地址;需通过__clone()方法手动实现深拷贝,逐个clone引用属性,并妥善处理不可克隆资源。

PHP对象克隆默认是浅拷贝,clone关键字不递归复制引用属性
直接用 clone 得到的对象,其内部的数组、对象等引用类型属性仍指向原对象的同一内存地址。改新对象的某个嵌套对象属性,原对象也会跟着变——这不是你想要的“真正独立副本”。
典型错误现象:__clone 方法没重写,或只写了空方法,但属性里有 $this->config 这样的对象,结果两个实例共用一个配置对象。
- 深拷贝必须手动处理每个引用型属性
-
__clone()是唯一被自动调用的克隆钩子,不写它就走默认浅拷贝逻辑 - 注意:不能在
__clone()里调用clone $this,会无限递归
在__clone中逐个clone引用属性才能实现深拷贝
PHP 不提供内置深拷贝函数,必须显式对每个需要隔离的引用属性调用 clone。基本模式就是:在 __clone() 里把要深拷的属性一个个重新赋值。
public function __clone() {
if ($this->handler instanceof Closure) {
// Closure 不能 clone,通常需重建或设为 null
$this->handler = null;
}
if (is_object($this->config)) {
$this->config = clone $this->config;
}
if (is_array($this->items)) {
// 数组里可能含对象,需遍历 clone
foreach ($this->items as $k => $v) {
if (is_object($v)) {
$this->items[$k] = clone $v;
}
}
}
}
- 只对确定可 clone 的对象调用
clone,否则抛Fatal error: Uncaught Error: Trying to clone an uncloneable object - 资源(resource)、闭包(Closure)、PDO 实例等不可 clone,得特殊处理(重建、置空、抛异常)
- 避免无脑递归 clone —— 比如循环引用结构会直接崩溃
用serialize/unserialize做通用深拷贝有兼容性陷阱
有人用 unserialize(serialize($obj)) 替代手写 __clone,确实能绕过引用问题,但它依赖类实现 __sleep/__wakeup,且不适用于所有场景。
- 资源类型(如文件句柄、数据库连接)无法序列化,会丢失或报错
- 类中含
private或protected属性时,serialize保留可见性标识,反序列化后行为正常;但若类定义变更,容易出错 - 性能差:大对象反复序列化开销明显,比手写
__clone慢 3–10 倍 - 如果类用了
__sleep忽略了某些关键属性,反序列化后对象状态就不完整
第三方库如deep-copy/deep-copy适合复杂嵌套但引入额外依赖
当对象图很深、类型混杂(含 Closure、PDO、SplObjectStorage 等),手写 __clone 维护成本高,可考虑 deep-copy/deep-copy 库。
它通过反射+白名单/黑名单机制控制克隆行为,支持跳过资源、自定义克隆逻辑、处理循环引用。
- 安装:
composer require myclabs/deep-copy - 基本用法:
$copier = new \DeepCopy\DeepCopy(); $newObj = $copier->copy($originalObj); - 缺点:增加 autoloader 负担;调试时堆栈更难追踪;某些封闭环境(如部分 SaaS 平台)不允许 Composer 依赖
- 不是银弹——遇到未注册的自定义资源或魔术方法副作用,仍需手动干预
真正难的从来不是“怎么克隆”,而是判断哪些属性该深、哪些该共享、哪些根本不能碰。别迷信自动方案,先理清业务语义里“副本”的边界在哪。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











