数字营销 · Web开发 · 基础设施

深复制和浅复制

通过 Python、JavaScript 和 PHP 的实例,对比深复制和浅复制的区别,以及引用数据变化对复制对象产生的影响。

在面向对象的编程中,经常会遇到对象克隆的问题。如果不了解深复制和浅复制(也就是深拷贝和浅拷贝)的概念,编写程序的时候就可能出现一些比较难发现的问题。我们知道,对象可以看成是由一系列 keyvalue 组成的,不同编程语言对于数据类型的划分并不完全一样。有些语言中所有数据都可以看成对象,例如 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]]

可以看到,cd 分别是 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 不相等

从这里可以看到,bc 本身已经是两个对象。修改 b.rank 时,c.rank 不会受到影响。

但是:

b.info.website ="www.hitoy.org";

修改的是 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

欢迎分享你的看法,也欢迎补充不同的实践经验。