纳瓦尔艺术免安装绿色中文版
2.82G · 2025-10-24
学习 PHP 面向对象编程(OOP)时,对象复制、对象比较、对象与引用、对象序列化是四个绕不开的核心概念。它们不仅在面试中频繁出现,更是日常开发的基石。本文将用最通俗、最全面的方式,结合代码示例和生活比喻,带你彻底掌握这四大知识点,看完即可融会贯通。
对象复制不是简单的“Ctrl+C, Ctrl+V”。它分为浅复制和深复制,核心在于如何处理对象内部的复杂属性(如其他对象或资源)。
clone 关键字机制:
clone 关键字创建一个新的对象实体。代码示例:
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!因为电池是共享的。
生活比喻:复印身份证
__clone() 魔术方法机制:
__clone() 是一个魔术方法,在 clone 操作完成后自动调用。__clone() 方法内部,你可以手动对内部的复杂对象属性执行 clone 操作,从而创建一个全新的、独立的副本。代码示例:
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(),以保证全局唯一性。PHP 提供了两种比较运算符:== (相等) 和 === (全等)。它们在比较对象时行为不同。
=== (全等比较) - “是同一个实体吗?”规则:
代码示例:
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 (克隆出来的是新对象)
== (相等比较) - “内容和类型都相同吗?”规则:
== 比较)。代码示例:
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,因为它们是不同的类。Person 有一个 private $age,而 Employee 没有,或者值不同,比较结果就是 false。== 比较。总结对比表:
| 比较方式 | 问题 | 检查点 | 示例 ($p1 = new P(); $p2 = new P();) |
|---|---|---|---|
=== | “是同一个东西吗?” | 内存地址(对象句柄) | false (不同实例) |
== | “是同款且配置一样吗?” | 1. 是否同个类 2. 所有属性值是否相等 | true (同类同属性) |
“PHP 中对象是通过引用传递的”——这是一个流传甚广但不准确的说法。正确的理解是:PHP 对象变量存储的是对象的“句柄”(标识符/ID),对象操作是通过这个句柄进行的,而句柄的赋值和传递是“按值传递”的。
当你执行 $obj = new MyClass(); 时:
MyClass 的实例(对象数据)。$obj 存储的不是对象本身,而是这个轻量级的对象句柄。$obj2 = $obj1;:这行代码复制了对象句柄的值。$obj1 和 $obj2 是两个独立的变量,但它们存储着相同的句柄值,因此都能访问同一个对象。function foo($param) { ... } foo($obj); 在调用时,$obj 的句柄值被复制给函数参数 $param。$param 和外部的 $obj 是不同的变量,但句柄相同。return $obj; 返回的是句柄的副本。代码示例 (句柄传递):
class Counter {
public $count = 0;
}
function increment($counter) { // $counter 是句柄的副本
$counter->count++; // 通过句柄修改对象
}
$c = new Counter();
increment($c);
echo $c->count; // 输出: 1
// 因为函数内通过句柄修改了对象,外部也能看到。
&& 符号创建变量的别名。两个变量名指向同一个变量存储位置。代码示例 (真正引用):
$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 值(特别是对象)转换为可存储或传输的字符串格式的过程。反序列化则是将其恢复。
serialize() - 打包成“时空胶囊”功能:
将 PHP 值转换为包含字节流的字符串。
对于对象,序列化字符串包含:
public、private、protected 属性的名称和值。不包含:
null 或丢失)。代码示例:
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";}
unserialize() - 解开“时空胶囊”功能:
为什么?
序列化字符串只记录了“这是个什么类型的对象”和“它的属性是什么”,但没有记录“这个对象能做什么”(方法)。
unserialize() 需要根据类名找到对应的类定义(“说明书”),然后:
代码示例 (正确):
// 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
代码示例 (错误 - 类未定义):
// 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! 方法不存在!
$_SESSION。必须在所有用到该对象的页面都 include 类文件!最佳实践:
确保类定义:反序列化前,务必通过 include, require 或 自动加载 (Autoloading) 加载类。
spl_autoload_register(function ($class_name) {
include $class_name . '.php';
});
// 这样,unserialize 时如果找不到类,会自动尝试加载。
使用 __sleep() 和 __wakeup() :
__sleep():在 serialize() 之前调用。常用于:
public function __sleep() {
// 不序列化数据库连接
// return ['name', 'secret', 'role']; // 只序列化这三个属性
$this->db = null; // 或者先关闭连接
return array_keys(get_object_vars($this)); // 序列化所有属性
}
__wakeup():在 unserialize() 之后调用。常用于:
__sleep() 中被关闭的资源。public function __wakeup() {
// 重新连接数据库
$this->db = mysqli_connect(...);
// 注意:这里无法恢复 $db 的具体状态,通常需要重新连接。
}
安全性:反序列化来自不可信来源的数据是极其危险的!攻击者可以构造恶意序列化字符串,导致任意代码执行(反序列化漏洞)。务必对输入进行严格验证,或使用更安全的格式(如 JSON)。
| 知识点 | 核心思想 | 关键函数/操作符 | 最佳实践 |
|---|---|---|---|
| 对象复制 | 独立性 浅复制共享内部,深复制完全独立 | clone, __clone() | 需要完全独立时,务必实现 __clone() 进行深复制 |
| 对象比较 | 同一性 vs. 相等性=== 看实体,== 看内容 | ===, == | 明确需求:判断是否同一个用 ===,判断内容是否相同用 == |
| 对象与引用 | 句柄 vs. 别名 对象操作靠句柄,句柄按值传递 | =, & | 理解“对象句柄按值传递”;& 用于创建变量别名 |
| 对象序列化 | 持久化与重建 打包状态,重建需“说明书” | serialize(), unserialize() | 反序列化前必须定义类;善用 __sleep()/__wakeup();警惕安全风险 |
2025-10-24
网易《逆水寒》手游与宇树科技达成合作,虚拟世界将成机器人技术试验田
2025-10-24
谷歌间接承认 Tensor G5 芯片存在 GPU 问题,将推送更新优化 Pixel 10 系列手机