学习 PHP 面向对象编程(OOP)时,对象复制、对象比较、对象与引用、对象序列化是四个绕不开的核心概念。它们不仅在面试中频繁出现,更是日常开发的基石。本文将用最通俗、最全面的方式,结合代码示例和生活比喻,带你彻底掌握这四大知识点,看完即可融会贯通。
一、对象复制:从"影子"到"独立个体"
对象复制不是简单的"Ctrl+C, Ctrl+V"。它分为浅复制 和深复制,核心在于如何处理对象内部的复杂属性(如其他对象或资源)。
1. 浅复制 (Shallow Copy) - clone
关键字
机制:
clone
关键字创建一个新的对象实体。- 对于基本类型属性(整数、字符串、布尔值),直接复制其值。
- 对于对象类型属性 (另一个对象),只复制其引用(标识符) ,而不是复制该对象本身。新旧对象共享内部对象。
代码示例:
php
class Battery {
public $level = 100;
}
class Robot {
public $name;
public $battery;
public function __construct($name, $battery) {
$this->name = $name;
$this->battery = $battery;
}
}
// 创建原机器人
$originalBattery = new Battery();
$originalRobot = new Robot("Alpha", $originalBattery);
// 浅复制
$clonedRobot = clone $originalRobot;
// $clonedRobot 是一个新机器人
// $clonedRobot->name 是 "Alpha" 的副本
// $clonedRobot->battery 指向的是 $originalBattery 的同一个对象!
// 修改克隆机器人的电池
$clonedRobot->battery->level = 50;
// 输出原机器人的电池电量
echo $originalRobot->battery->level; // 输出: 50!因为电池是共享的。
生活比喻:复印身份证
你有一个机器人,它用的是一块公共电池。你"复制"一个机器人,新机器人连的还是同一块公共电池。新机器人耗电,公共电池电量下降,原来的机器人电量也跟着变低。
2. 深复制 (Deep Copy) - __clone()
魔术方法
机制:
__clone()
是一个魔术方法 ,在clone
操作完成后自动调用。- 在
__clone()
方法内部,你可以手动对内部的复杂对象属性执行clone
操作,从而创建一个全新的、独立的副本。 - 这样,新旧对象及其所有内部对象都完全独立。
代码示例:
php
class Robot {
public $name;
public $battery;
public function __construct($name, $battery) {
$this->name = $name;
$this->battery = $battery;
}
// 自定义克隆,实现深复制
public function __clone() {
// 关键:把内部的 battery 对象也克隆一份!
$this->battery = clone $this->battery;
// 此时 $this 指向新创建的克隆对象 ($clonedRobot)
}
}
// ... 创建 $originalRobot 同上 ...
// 深复制
$clonedRobot = clone $originalRobot;
// 1. 创建新机器人 $clonedRobot。
// 2. 浅复制:$clonedRobot->name 复制值;$clonedRobot->battery 复制引用(暂时指向原电池)。
// 3. 自动调用 $clonedRobot->__clone()。
// 4. 在 __clone() 中:$clonedRobot->battery = clone $clonedRobot->battery;
// 这行代码:创建了一个新的 Battery 对象,并让 $clonedRobot->battery 指向它。
// 修改克隆机器人的电池
$clonedRobot->battery->level = 50;
// 输出原机器人的电池电量
echo $originalRobot->battery->level; // 输出: 100!因为电池是独立的。
生活比喻:克隆人 + 复制器官
你有一个机器人,它用一块专属电池。你用"克隆机"复制机器人,克隆机不仅复制机器人本体,还单独克隆出一块一模一样的新电池,并安装给克隆人。现在,两个机器人各有各的电池,互不影响。
深复制的注意事项:
- 递归性 :如果
Battery
类内部还有另一个对象(如$charger
),你也需要在Battery
的__clone()
方法中clone $this->charger
,否则charger
又会共享。深复制需要递归地处理所有对象属性。 - 资源 :对于数据库连接、文件句柄等非对象资源 ,
clone
通常无效或危险。应在__clone()
中将其设为null
或抛出异常,因为一个连接不能被两个对象同时使用。 - 单例模式 :单例类通常会禁用
clone
,在__clone()
中抛出异常或直接die()
,以保证全局唯一性。
二、对象比较:"同一性" vs. "相等性"
PHP 提供了两种比较运算符:==
(相等) 和 ===
(全等)。它们在比较对象时行为不同。
1. ===
(全等比较) - "是同一个实体吗?"
规则:
- 检查两个变量是否引用内存中的同一个对象实例。
- 本质上是比较它们的对象句柄(标识符)是否相同。
代码示例:
ruby
class Person {
public $name = "John";
}
$p1 = new Person();
$p2 = $p1; // $p2 是 $p1 的别名(引用同一个对象)
$p3 = new Person(); // $p3 是一个全新的对象
$p4 = clone $p1; // $p4 是 $p1 的克隆(新对象)
var_dump($p1 === $p2); // true ($p1 和 $p2 指向同一个对象)
var_dump($p1 === $p3); // false (两个不同的对象实例)
var_dump($p1 === $p4); // false (克隆出来的是新对象)
2. ==
(相等比较) - "内容和类型都相同吗?"
规则:
- 检查两个对象是否是同一个类的实例。
- 检查它们的所有属性值是否相等。
- 属性值的比较也是递归进行的(对于对象属性,会继续用
==
比较)。
代码示例:
ini
class Person {
public $name = "John";
}
$p1 = new Person();
$p2 = $p1; // 同一个实例
$p3 = new Person(); // 新实例,但属性相同
$p4 = clone $p1; // 克隆实例,属性相同
var_dump($p1 == $p2); // true (同实例,同属性)
var_dump($p1 == $p3); // true (同类,同属性值)
var_dump($p1 == $p4); // true (同类,同属性值)
// 不同属性值
$p5 = new Person();
$p5->name = "Jane";
var_dump($p1 == $p5); // false (属性值不同)
// 不同类
class Employee extends Person {
public $employeeId = 1;
}
$e1 = new Employee();
var_dump($p1 == $e1); // false (不是同一个类!)
==
比较的细节:
- 类必须相同 :即使
Employee
继承自Person
,$p1 == $e1
也是false
,因为它们是不同的类。 - 属性必须完全匹配 :不仅值要相等,属性的名称、数量、可见性(public/private/protected) 都需要匹配。如果
Person
有一个private $age
,而Employee
没有,或者值不同,比较结果就是false
。 - 递归比较 :如果属性是对象,会递归调用
==
比较。
总结对比表:
比较方式 | 问题 | 检查点 | 示例 ($p1 = new P(); $p2 = new P(); ) |
---|---|---|---|
=== |
"是同一个东西吗?" | 内存地址(对象句柄) | false (不同实例) |
== |
"是同款且配置一样吗?" | 1. 是否同个类 2. 所有属性值是否相等 | true (同类同属性) |
三、对象与引用:揭开"引用传递"的迷雾
"PHP 中对象是通过引用传递的"------这是一个流传甚广但不准确的说法 。正确的理解是:PHP 对象变量存储的是对象的"句柄"(标识符/ID),对象操作是通过这个句柄进行的,而句柄的赋值和传递是"按值传递"的。
1. 对象句柄 (Object Handle) - 真相
-
当你执行
$obj = new MyClass();
时:- PHP 在堆内存 中创建
MyClass
的实例(对象数据)。 - PHP 生成一个唯一的对象句柄(可以理解为内存地址或 ID)。
- 变量
$obj
存储的不是对象本身,而是这个轻量级的对象句柄。
- PHP 在堆内存 中创建
2. 句柄的"值传递" (Value Passing of Handles)
$obj2 = $obj1;
:这行代码复制了对象句柄的值 。$obj1
和$obj2
是两个独立的变量,但它们存储着相同的句柄值,因此都能访问同一个对象。- 函数传参 :
function foo($param) { ... } foo($obj);
在调用时,$obj
的句柄值被复制 给函数参数$param
。$param
和外部的$obj
是不同的变量,但句柄相同。 - 返回值 :
return $obj;
返回的是句柄的副本。
代码示例 (句柄传递):
php
class Counter {
public $count = 0;
}
function increment($counter) { // $counter 是句柄的副本
$counter->count++; // 通过句柄修改对象
}
$c = new Counter();
increment($c);
echo $c->count; // 输出: 1
// 因为函数内通过句柄修改了对象,外部也能看到。
3. 真正的引用 (Reference) - &
&
符号创建变量的别名 。两个变量名指向同一个变量存储位置。- 这与对象句柄的"值传递"有本质区别。
代码示例 (真正引用):
perl
$a = 10;
$b = $a; // $b 是 $a 的值的副本
$b = 20; // 改 $b 不影响 $a
echo $a; // 输出: 10
$c = 10;
$d = &$c; // $d 是 $c 的引用(别名)
$d = 20; // 改 $d 就是改 $c
echo $c; // 输出: 20
// 对象 + 引用
$obj1 = new Counter();
$obj2 = $obj1; // $obj2 拿到句柄副本 (句柄传递)
$obj3 = &$obj1; // $obj3 是 $obj1 的引用 (别名)
$obj2->count = 100; // 通过句柄修改对象
echo $obj1->count; // 输出: 100 (通过 $obj1 的句柄访问)
$obj3->count = 200; // 通过 $obj3 (即 $obj1) 的句柄修改
echo $obj1->count; // 输出: 200
// 关键区别:重新赋值
$obj2 = new Counter(); // $obj2 换了个新句柄,$obj1 不变
$obj3 = new Counter(); // $obj3 是 $obj1 的别名,所以 $obj1 也被赋了新值!
// 此时 $obj1 指向了新对象,$obj3 也指向新对象。
核心对比:
操作 | 机制 | 重新赋值的影响 |
---|---|---|
$obj2 = $obj1; (对象) |
句柄的值传递 复制句柄,两个变量独立 | $obj2 = new X(); 只改变 $obj2 ,$obj1 不变 |
$var2 = &$var1; |
创建引用 两个变量名共享一个存储位置 | $var2 = new X(); 会同时改变 $var1 和 $var2 |
结论: 说"对象按引用传递"是一种方便但不精确的说法。准确的说法是"对象句柄按值传递 "。理解这一点,能避免很多因误解
&
而产生的错误。
四、对象序列化:对象的"时空穿梭术"
序列化是将 PHP 值(特别是对象)转换为可存储或传输的字符串格式的过程。反序列化则是将其恢复。
1. serialize()
- 打包成"时空胶囊"
功能:
-
将 PHP 值转换为包含字节流的字符串。
-
对于对象,序列化字符串包含:
- 类名 (Class Name) :对象的类型。
- 所有属性 (Properties) :包括
public
、private
、protected
属性的名称和值。
-
不包含:
- 对象的方法 (Methods) 。
- 静态属性 (Static Properties) 。
- 资源 (Resources) 如文件句柄、数据库连接(序列化时通常为
null
或丢失)。
代码示例:
php
class User {
public $name = "Alice";
private $secret = "top_secret";
protected $role = "user";
public static $count = 0; // 静态属性
public $db; // 资源,假设是一个数据库连接
public function __construct($db) {
$this->db = $db;
}
}
$db = mysqli_connect(...); // 假设有一个数据库连接
$user = new User($db);
$s = serialize($user);
// $s 字符串会包含类名 'User' 和三个属性 (name, secret, role) 的值。
// $count (静态) 和 $db (资源) 不会被有效序列化。
echo $s; // 输出类似:O:4:"User":3:{s:4:"name";s:5:"Alice";s:11:"Usersecret";s:10:"top_secret";s:9:"*role";s:4:"user";}
2. unserialize()
- 解开"时空胶囊"
功能:
- 将序列化字符串转换回 PHP 值。
- 最关键的规则:反序列化一个对象时,该对象的类必须在反序列化之前就已经定义好了!
为什么?
-
序列化字符串只记录了"这是个什么类型的对象 "和"它的属性是什么 ",但没有记录"这个对象能做什么"(方法)。
-
unserialize()
需要根据类名找到对应的类定义("说明书"),然后:- 创建该类的一个新实例。
- 将序列化字符串中的属性值填充到这个新实例中。
代码示例 (正确):
php
// page1.php - 序列化
include "User.php"; // 必须先加载类定义
$user = new User($db);
$s = serialize($user);
file_put_contents('user.dat', $s);
// page2.php - 反序列化
include "User.php"; // 必须再次加载类定义!
$s = file_get_contents('user.dat');
$user = unserialize($s); // 成功!$user 是一个功能完整的 User 对象
echo $user->name; // 输出: Alice
代码示例 (错误 - 类未定义):
perl
// page2_bad.php - 反序列化 (缺少 include)
// include "User.php"; // 忘记了!
$s = file_get_contents('user.dat');
$user = unserialize($s); // PHP 不认识 'User' 类!
// $user 的类型是 __PHP_Incomplete_Class
var_dump($user);
// 输出类似:object(__PHP_Incomplete_Class)#1 (3) { ["__PHP_Incomplete_Class_Name"]=> string(4) "User" ["name"]=> string(5) "Alice" ... }
// $user->name; // 可以访问属性
// $user->someMethod(); // Fatal error! 方法不存在!
3. 应用场景与最佳实践
- Session 会话 :将用户对象存入
$_SESSION
。必须在所有用到该对象的页面都include
类文件! - 缓存:将数据库查询结果(对象数组)序列化后存入 Memcached/Redis,避免重复查询。
- 数据持久化:将配置对象存入文件。
- 跨系统通信:通过 API 传输对象状态(虽然 JSON 更常用)。
最佳实践:
-
确保类定义 :反序列化前,务必通过
include
,require
或 自动加载 (Autoloading) 加载类。phpspl_autoload_register(function ($class_name) { include $class_name . '.php'; }); // 这样,unserialize 时如果找不到类,会自动尝试加载。
-
使用
__sleep()
和__wakeup()
:-
__sleep()
:在serialize()
之前调用。常用于:- 关闭数据库连接等资源。
- 返回一个数组,指定哪些属性需要被序列化(实现选择性序列化)。
kotlinpublic function __sleep() { // 不序列化数据库连接 // return ['name', 'secret', 'role']; // 只序列化这三个属性 $this->db = null; // 或者先关闭连接 return array_keys(get_object_vars($this)); // 序列化所有属性 }
-
__wakeup()
:在unserialize()
之后调用。常用于:- 重新建立数据库连接。
- 重新初始化在
__sleep()
中被关闭的资源。
phppublic function __wakeup() { // 重新连接数据库 $this->db = mysqli_connect(...); // 注意:这里无法恢复 $db 的具体状态,通常需要重新连接。 }
-
-
安全性 :反序列化来自不可信来源的数据是极其危险的!攻击者可以构造恶意序列化字符串,导致任意代码执行(反序列化漏洞)。务必对输入进行严格验证,或使用更安全的格式(如 JSON)。
🎯 终极总结与学习路线
知识点 | 核心思想 | 关键函数/操作符 | 最佳实践 |
---|---|---|---|
对象复制 | 独立性 浅复制共享内部,深复制完全独立 | clone , __clone() |
需要完全独立时,务必实现 __clone() 进行深复制 |
对象比较 | 同一性 vs. 相等性 === 看实体,== 看内容 |
=== , == |
明确需求:判断是否同一个用 === ,判断内容是否相同用 == |
对象与引用 | 句柄 vs. 别名 对象操作靠句柄,句柄按值传递 | = , & |
理解"对象句柄按值传递";& 用于创建变量别名 |
对象序列化 | 持久化与重建 打包状态,重建需"说明书" | serialize() , unserialize() |
反序列化前必须定义类;善用 __sleep() /__wakeup() ;警惕安全风险 |