🚀 PHP 面向对象四大核心知识点全面详解

学习 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(); 时:

    1. PHP 在堆内存 中创建 MyClass 的实例(对象数据)。
    2. PHP 生成一个唯一的对象句柄(可以理解为内存地址或 ID)。
    3. 变量 $obj 存储的不是对象本身,而是这个轻量级的对象句柄

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 值转换为包含字节流的字符串。

  • 对于对象,序列化字符串包含:

    1. 类名 (Class Name) :对象的类型。
    2. 所有属性 (Properties) :包括 publicprivateprotected 属性的名称和值
  • 不包含

    • 对象的方法 (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() 需要根据类名找到对应的类定义("说明书"),然后:

    1. 创建该类的一个新实例。
    2. 将序列化字符串中的属性值填充到这个新实例中。

代码示例 (正确):

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 更常用)。

最佳实践:

  1. 确保类定义 :反序列化前,务必通过 include, require自动加载 (Autoloading) 加载类。

    php 复制代码
    spl_autoload_register(function ($class_name) {
        include $class_name . '.php';
    });
    // 这样,unserialize 时如果找不到类,会自动尝试加载。
  2. 使用 __sleep()__wakeup()

    • __sleep():在 serialize() 之前调用。常用于:

      • 关闭数据库连接等资源。
      • 返回一个数组,指定哪些属性需要被序列化(实现选择性序列化)。
      kotlin 复制代码
      public function __sleep() {
          // 不序列化数据库连接
          // return ['name', 'secret', 'role']; // 只序列化这三个属性
          $this->db = null; // 或者先关闭连接
          return array_keys(get_object_vars($this)); // 序列化所有属性
      }
    • __wakeup():在 unserialize() 之后调用。常用于:

      • 重新建立数据库连接。
      • 重新初始化在 __sleep() 中被关闭的资源。
      php 复制代码
      public function __wakeup() {
          // 重新连接数据库
          $this->db = mysqli_connect(...);
          // 注意:这里无法恢复 $db 的具体状态,通常需要重新连接。
      }
  3. 安全性 :反序列化来自不可信来源的数据是极其危险的!攻击者可以构造恶意序列化字符串,导致任意代码执行(反序列化漏洞)。务必对输入进行严格验证,或使用更安全的格式(如 JSON)。


🎯 终极总结与学习路线

知识点 核心思想 关键函数/操作符 最佳实践
对象复制 独立性 浅复制共享内部,深复制完全独立 clone, __clone() 需要完全独立时,务必实现 __clone() 进行深复制
对象比较 同一性 vs. 相等性 === 看实体,== 看内容 ===, == 明确需求:判断是否同一个用 ===,判断内容是否相同用 ==
对象与引用 句柄 vs. 别名 对象操作靠句柄,句柄按值传递 =, & 理解"对象句柄按值传递";& 用于创建变量别名
对象序列化 持久化与重建 打包状态,重建需"说明书" serialize(), unserialize() 反序列化前必须定义类;善用 __sleep()/__wakeup();警惕安全风险
相关推荐
用户3074596982074 小时前
🐶🐱 协变与逆变:用“动物收容所”讲清楚 PHP 类型的“灵活继承”
php
herderl6 小时前
【无标题】命名管道(Named Pipe)是一种在操作系统中用于**进程间通信(IPC)** 的机制
java·linux·服务器·嵌入式硬件·php
云博客-资源宝13 小时前
php防注入和XSS过滤参考代码
开发语言·php·xss
huluang16 小时前
PHP版本控制系统:高效文档管理
开发语言·php
Bruce_Liuxiaowei1 天前
绕过文件上传漏洞并利用文件包含漏洞获取系统信息的技术分析
运维·网络安全·php·apache
用户3074596982071 天前
🌟 PHP 重载(Overloading)——不是你想的那样!
php
用户3074596982071 天前
🌟 匿名类(Anonymous Class)——“一次性用完就扔的小纸条”
php
钢铁男儿2 天前
C# 异步编程:提升程序性能与用户体验的利器
c#·php·ux
暗流者2 天前
信息安全简要
开发语言·网络·php