PHP对象的克隆与引用有什么区别 - PHP 面试题

这道 PHP 面试题确切说应该这样描述:“ PHP 对象的赋值和克隆有什么区别 ”,注意不是复制,就是复制,打开窗子说亮话,就是下面两行有什么区别。

class User{
  public $username = 'test';
}
$user1 = new User();
// 注意下面两行,有啥子区别
$user2 = $user1;
$user2 = clone $user1;

$user2 = $user1这种写法,实际上是引用写法,也就是说本质上user1和user2变量指向的都是同一个PHP对象,占用的内存也只有一份,如果你修改user2的username属性实际上就是在user1的username属性,当然了,修改user1的username属性也是在修改user2的username属性,搞来搞去都是同一个。

然而,$user2 = clone $user1这种写法表示实打实地复制一个User对象出来,而不是普普通通的引用了,这种情况下,你修改user2的username属性不会影响user1的username属性,修改user1的任何属性也不会影响到user2的属性,说到底是占用了两份内存。但是,要值得注意的是,你执行完毕clone后系统也并不会真的立马就开辟一块儿新的内存用于存放新的对象。如果你clone完这个对象后,并没有修改、添加、删除任何属性或者方法,这两个对象在内存依然为同一个,只有在你对任意一个对象的属性或者方法进行增删改的时候,系统才会真的再开辟一块儿内存复制对象,这种技术叫做写实拷贝,是节约内容的一大利器!很多地方都会利用这种技术,大家要记住。

php中有个魔术方法叫做 clone,这个方法呢,只有在你执行clone的时候,会被自动触发,如果你要对某个类在被克隆的时候做一些特殊操作,那么你就可以在这个类中自定义一些业务逻辑进来,这个具体就要看你的业务场景了。注意,如果你在clone魔术方法中直接修改类的属性,他修改的是新复制出来的类的属性,而不是老类的属性,用上面的例子就是会修改user2的属性,而不是user1的属性,具体代码如下:

<?php
class User{
  public $username = 'test';
  public function __clone() {
    $this->username = 'www';
  }
}
$user1 = new User();
// 注意下面两行,有啥子区别
$user2 = $user1;
$user2 = clone $user1;

__clone 方法中修改 username 属性为 www,修改的是 user2 对象,而不是 user1!

$user2 = $user1,就是传说中的浅复制。 $user2 = clone $user1,就是传说中的深复制。

然而,深复制依然有特例,比如下面这坨代码:

<?php
class People {
  public $age = 18;
}
class User {
  public $username = 'zhangsan';
  public $peple = null;
  public function __construct() {
    $this->people = new People();
  }
}
$user1 = new User();
$user2 = clone $user1;
$user1->username = 'lisi';
$user1->people->age = 22;
echo $user1->username.PHP_EOL;
echo $user1->people->age.PHP_EOL;
echo $user2->username.PHP_EOL;
echo $user2->people->age.PHP_EOL;

结果是啥,你们自己可以运行一下,我只说结果,反正就是 user1 和 user2 的 username 属性确实是不受影响了,但是 people 属性却是指向的同一个,这个十分尴尬,也就说即便是深复制,当类的某个属性为另外一个对象的时候,默认只会浅复制这个对象属性,那么如何彻底地复制对象类型的属性呢?还得靠 __clone 魔术方法...

<?php
class People {
  public $age = 18;
}
class User {
  public $username = 'zhangsan';
  public $peple = null;
  public function __construct() {
    $this->people = new People();
  }
  public function __clone() {
    $this->people = new People();
  }
}
$user1 = new User();
$user2 = clone $user1;
$user1->username = 'lisi';
$user1->people->age = 22;
echo $user1->username.PHP_EOL;
echo $user1->people->age.PHP_EOL;
echo $user2->username.PHP_EOL;
echo $user2->people->age.PHP_EOL;

运行一下,感受一下结果。

那么,对象的克隆有什么用处呢?

举个例子吧,比如某个控制器方法中,你通过查询数据库返回了一个数据对象为$user,然后你直接echo json_encode( $user )给客户端,但是在输出之前呢,你还需要对这个$user进行一些业务逻辑运算,比如对某个属性加1,然后存入到redis中,但是客户端又需要的是最原始的从数据库取出的状态,那么这个时候,你就只能 clone 这个对象,原对象用于输出给客户端,另外一个备份对象用于redis存储。

在我们公司,也遇到大量的clone,都是用于构建请求对象,和上面这种应用场景是非常类似的,构建出一个原始对象,用于形成最终数据给客户端,同时克隆一个这个原始对象用于逻辑业务处理生成另外一个结果。