在面向对象的编程中,经常会遇到对象克隆的问题。如果不了解深复制和浅复制(也就是深拷贝和浅拷贝)的概念,编写程序的时候就可能出现一些比较难发现的问题。我们知道,对象可以看成是由一系列 key、value 组成的,不同编程语言对于数据类型的划分并不完全一样。有些语言中所有数据都可以看成对象,例如 Python;有些语言则进行了其他的数据类型划分。但是从数据或者对象的存储方式来看,基本可以把数据类型理解成值类型和引用类型。一般来说,值类型的数据存储在栈内存中,而引用类型的数据存储在堆内存中,如下图:

如果一个对象中的所有数据都是值类型的数据,那么就不会出现深复制和浅复制的问题。例如:
[1,2,3,4,5,6]
无论怎么复制,实际上都是重新得到一份数据。另一种情况是,如果一个对象中存在引用类型的数据,那么在复制这个对象的时候,这些引用型数据到底是重新复制一份,还是继续使用原来的引用?这里就是深复制和浅复制的区别。
如果把对象中的所有引用数据都重新复制一份,这就是深复制。
如果只是复制引用,让新的对象继续指向原来的数据,那么就是浅复制。
由此可以看到:
深复制不会受到原来引用数据变化的影响,而浅复制则不然。
如果原来的引用型数据发生改变,那么浅复制出来的数据也可能跟着发生变化。
Python中的深复制和浅复制
首先来看一下 Python 的例子:
#!/usr/bin/env python
import copy
a = [1,2,3,4,5,6]
b = ['a','b','c',a]
c = copy.copy(b)
d = copy.deepcopy(b)
print c
# ['a', 'b', 'c', [1, 2, 3, 4, 5, 6]]
print d
# ['a', 'b', 'c', [1, 2, 3, 4, 5, 6]]
a.append(7)
print c
# ['a', 'b', 'c', [1, 2, 3, 4, 5, 6, 7]]
print d
# ['a', 'b', 'c', [1, 2, 3, 4, 5, 6]]
可以看到,c 和 d 分别是 b 的浅复制和深复制。
而 b 中又包含了 a。
当 a 发生变化之后:
a → 发生变化
│
├── c:引用了原来的 a → 跟着变化
│
└── d:复制了新的数据 → 不发生变化
这就是深复制和浅复制最直观的区别。
JavaScript中的深复制和浅复制
JavaScript 的数据类型虽然和 Python 有一些区别。JavaScript 把数据划分为原始数据类型和引用数据类型,原始数据类型并不是严格意义上的对象,而 Python 中则可以把所有数据看成对象。但是,JavaScript 同样存在深复制和浅复制的问题。当时 JavaScript 并没有提供类似 Python copy()、deepcopy() 这样的直接方法,所以可以自己实现。
首先来看一个简单的复制方法:
Object.prototype.copy = function(){
var newobj = new Object();
for(var i in this){
newobj[i] = this[i];
}
return newobj;
}
var a = {
"name":"Hito",
"website":"www.hitoy.org"
};
var b = {
"rank":1,
"info":a
};
var c = b.copy();
b.info.website = "www.hitoy.org";
console.log(b.info.website);
console.log(c.info.website);
// b 和 c 的 info 相等
b.rank = 2;
console.log(b.rank);
console.log(c.rank);
// b 和 c 的 rank 不相等从这里可以看到,b 和 c 本身已经是两个对象。修改 b.rank 时,c.rank 不会受到影响。
但是:
修改的是 b.info 所引用的对象,因此 c.info.website 也会发生变化。
这就是浅复制。那么深复制呢?可以继续进行递归复制:
Object.prototype.deepcopy = function(){
var newobj = new Object();
for(var i in this){
if(typeof(this[i]) == "object"){
newobj[i] = this[i].deepcopy();
}else{
newobj[i] = this[i];
}
}
return newobj;
}
var a = {
"name":"Hito",
"website":"www.hitoy.org"
};
var b = {
"rank":1,
"info":a
};
var d = b.deepcopy();
b.info.website = "www.hitoy.com";
console.log(b.info.website);
console.log(d.info.website);
// b 和 d 不相等这里在复制 b 的时候,连 b.info 所引用的数据也重新复制了一份,所以之后修改:
b.info.website
不会影响 d.info.website。
PHP中的深复制和浅复制
和 JavaScript 一样,PHP 也没有提供一个简单的 deepcopy() 或 copy() 方法。PHP 的数据类型和 Python、JavaScript 又存在一些区别。按照这里的理解,可以简单把 PHP 数据分为标量类型和复合类型,其中数组可以看成值类型,而类的实例化对象属于引用型数据。下面来看一下 PHP 中是怎么实现深复制和浅复制的。
首先是浅复制:
<?php
class A{
public $varb = 1;
}
class B{
public $object1;
}
$b = new B();
$bb = new A();
$b->object1 = $bb;
$c = clone $b;
print_r($b->object1);
print_r($c->object1);
// 上面两个看起来相同
$bb->varb = 2;
print_r($b->object1);
print_r($c->object1);
// 上面两个也相等
这里通过 $c = clone $b 复制了 $b。
但是 $b->object1 本身又指向另外一个对象 $bb。所以复制之后,两个对象中的 object1 仍然指向同一个引用。因此,当:
$bb->varb = 2;
之后,$b->object1 和 $c->object1 都会发生变化。
这就是浅复制。如果希望把里面的对象也复制一份,就可以利用 __clone():
<?php
class A{
public $varb = 1;
}
class B{
public $object1;
public function __clone(){
$this->object1 = clone $this->object1;
}
}
$b = new B();
$bb = new A();
$b->object1 = $bb;
$c = clone $b;
print_r($b->object1);
print_r($c->object1);
// 上面两个相同
$bb->varb = 2;
print_r($b->object1);
print_r($c->object1);
// 上面两个已经不相同
?>
这里在复制 B 对象的时候,又对 object1 进行了 clone,因此内部的 A 对象也被重新复制了一份。
这样,修改原来的 $bb 时,新的对象就不会跟着发生变化。
总结
通过 Python、JavaScript 和 PHP 的这几个例子,可以比较直观地理解深复制和浅复制。
简单来说:
复制对象
↓
引用数据仍然指向原来的对象
而深复制:
复制对象
↓
引用数据也重新复制
↓
彼此不再影响
所以,在对象中存在引用类型数据的时候,深复制和浅复制的区别就非常重要。
评论0
欢迎分享你的看法,也欢迎补充不同的实践经验。