🚀 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();警惕安全风险
相关推荐
BingoGo1 天前
当你的 PHP 应用的 API 没有限流时会发生什么?
后端·php
JaguarJack1 天前
当你的 PHP 应用的 API 没有限流时会发生什么?
后端·php·服务端
BingoGo2 天前
OpenSwoole 26.2.0 发布:支持 PHP 8.5、io_uring 后端及协程调试改进
后端·php
JaguarJack2 天前
OpenSwoole 26.2.0 发布:支持 PHP 8.5、io_uring 后端及协程调试改进
后端·php·服务端
JaguarJack3 天前
推荐 PHP 属性(Attributes) 简洁读取 API 扩展包
后端·php·服务端
BingoGo3 天前
推荐 PHP 属性(Attributes) 简洁读取 API 扩展包
php
JaguarJack4 天前
告别 Laravel 缓慢的 Blade!Livewire Blaze 来了,为你的 Laravel 性能提速
后端·php·laravel
郑州光合科技余经理5 天前
代码展示:PHP搭建海外版外卖系统源码解析
java·开发语言·前端·后端·系统架构·uni-app·php
QQ5110082855 天前
python+springboot+django/flask的校园资料分享系统
spring boot·python·django·flask·node.js·php
WeiXin_DZbishe5 天前
基于django在线音乐数据采集的设计与实现-计算机毕设 附源码 22647
javascript·spring boot·mysql·django·node.js·php·html5