JavaScript 包含了多种数据类型,它们大体可以归为两大类,即 基本数据类型(String,Number,Boolean,null,undefined)、引用数据类型(Object)。其中,引用数据类型主要以 对象(Object)、函数(Funciton)、数组(Array)以及 正则(Regexp)这些内置对象为代表。

基本数据类型的赋值比较简单,1 就是 1,2 就是 2,变量之间不会互相影响:

var a = 1,
b;

b = a;
console.log(b); // 1

b = 2;
console.log(a, b); // 1 2

一开始将 a 的值赋值给 b,b 的值就变成 1。当 b 重新赋值为 2 时,a 的值仍然为 1。

但引用数据类型就没那么简单了,正是如此,很多JavaScript书籍都花大篇幅来讲解引用数据类型。

我们知道,当定义一些JavaScript变量时,这些变量会存在内存中。但 基本数据类型 和 引用数类型 存储在内存中的数据结构形式是有差异的。

基本数据类型是存在 栈内存,引用数据类型存在于 堆内存。

基本数据类型的赋值,就是简单的复制内容。但引用数据类型的赋值,不仅复制内容,而且,它还复制引用关系。这种引用关系,是原数据指向内存地址的指针。

来看下对象的拷贝:

var obj1 = {}, obj2 = {};

console.log(obj1 === obj2); // false

上面的代码,obj1 和 obj2 虽然都是新建的空对象,但它们并不相等。因为它们同时开辟了两块内存空间,来存储这两个对象。

再来看对象的赋值:

var person1 = {company: 'xx', job: 'web', name: 'tom'},
person2 = {};

person2 = person1;
console.log(person2 === person1); // true

person2.name = 'john';
console.log(person1, person2);
// {company: "xx", job: "web", name: "john"}
// {company: "xx", job: "web", name: "john"}

person1.age = 29;
console.log(person1, person2);
// {company: "xx", job: "web", name: "john", age: 29}
// {company: "xx", job: "web", name: "john", age: 29}

在这里,为了让对象 person1 和对象 person2 共享 company、job 的值,我们将 person1 对象的内容赋值到 person2 上,对两者进行对比,它们竟然完全相等。

当通过 person2.name = 'john' 来改变 name 属性时,person1 对象的 name 属性的值也变成了 mol。同时,我们又给 person1 对象新增一个 age 属性,此时,person2 对象也新增了 age 属性。

这说明 person2 和 person1 占用的是同一块内存地址,person2 的指针指向存储 person1 的内存地址。

这样,也导致了原对象和新对象的相互影响。

浅拷贝

但大多数情况下,我们只想拷贝相同的内容,而并不想原对象受到新对象的影响。

因为 person1 和 person2 根本就是两个人,他们有共性(company、job),也有个性(name、age)。在处理每个对象时,当然希望他们的“个性”互不相关。

我们常规的做法是,新建一个对象来拷贝原对象的所有属性。这样,就相当于把对象之间的赋值,分解为一个个基本数据类型(对象属性)的赋值。

function shallowCopy(origin) {
var copy = {};

for (var prop in origin) {
if (origin.hasOwnProperty(prop)) { // 只针对自身属性,忽略对象的原型属性
copy[prop] = origin[prop];
}
}

return copy;
}

var person1 = {company: 'xx', job: 'web', name: 'tom'},
person2 = {};

person2 = shallowCopy(person1);
console.log(person2 === person1); // false

person2.name = 'john';
console.log(person1, person2);
// {company: "xx", job: "web", name: "tom"}
// {company: "xx", job: "web", name: "john"}

person1.age = 29;
console.log(person1, person2);
// {company: "xx", job: "web", name: "tom", age: 29}
// {company: "xx", job: "web", name: "john"}

这种形式的拷贝称之为浅拷贝。

浅拷贝的问题

当然,有的时候我们需要拷贝的对象没那么简单,对象的属性值可能还嵌套着对象或者数组,或者多重嵌套。那上面的函数代码还能正常运行吗?

来看个例子:

function shallowCopy(origin) {
var copy = {};

for (var prop in origin) {
if (origin.hasOwnProperty(prop)) { // 只针对自身属性,忽略对象的原型属性
copy[prop] = origin[prop];
}
}

return copy;
}

var person1 = {company: 'xx', name: 'tom', contact: {tel: 1234, title: '...'}},
person2 = {};

person2 = shallowCopy(person1);

person2.name = 'john';
person2.contact.tel = 4321;

console.log(person1, person2);
// {company: 'xx', name: 'tom', contact: {tel: 4321, title: '...'}}
// {company: 'xx', name: "john", contact: {tel: 4321, title: '...'}}

上面的代码中,我们还是将 person1 对象的属性拷贝给 person2, person1 的 name 属性并没有因 person2 的 name 值发生变化而变化。

不同的是,原对象包含了一个 contact 属性,它的值是一个对象,当我们改变 person2 中 contact 对象下的 tel 属性时,person1 也跟着发生改变了。

这显然不是我们想要的结果。此时,你需要 深拷贝。

深拷贝

所谓的 深拷贝,就是将原对象的所有属性(包括嵌套的基本数据类型、引用数据类型)都拷贝一遍。来看下面代码:

function deepCopy(origin) {
// 当 origin 为子属性时,它的值可能为对象,也可能为数组,for in 后结构不同
var copy = Array.isArray(origin) ? [] : {};

for (var prop in origin) {
if (origin.hasOwnProperty(prop)) { // 只针对自身属性,忽略对象的原型属性
// 子属性值为对象时,对属性进行递归,直到基本数据类型
copy[prop] = typeof origin[prop] === 'object' ? deepCopy(origin[prop]) : origin[prop];
}
}

return copy;
}

var person1 = {company: 'xx', name: 'tom', contact: {tel: 1234, title: ['P3']}},
person2 = {};

person2 = deepCopy(person1);

person2.name = 'john';
person2.contact.tel = 4321;
person2.contact.title.push('M1');

console.log(person1, person2);
// {company: 'xx', name: 'tom', contact: {tel: 1234, title: ['P3']}}
// {company: 'xx', name: "john", contact: {tel: 4321, title: ['P3', 'M1']}}

上面的 deepCopy 函数实现了深拷贝,这样一来,对于原对象而言,无论是深层属性、还是属性的值是否对象,统统不会受新对象的影响。

需要注意的是,这种形式的拷贝,因为需要递归所有属性,所以,它比较耗性能。

除了上面的方法。有的人还利用 JSON 的原型方法来实现 深拷贝。即先序列化,再反序列化:

var person1 = {company: 'xx', contact: {tel: 1234, title: '...'}},
person2 = {};

person2 = JSON.parse(JSON.stringify(person1));

person2.contact.tel = 4321;

console.log(person1, person2);
// {company: 'xx', contact: {tel: 1234, title: '...'}}
// {company: 'xx', contact: {tel: 4321, title: '...'}}

总得来讲,浅拷贝是新对象只复制原对象最外层的属性,而深拷贝则是递归复制原对象的所有属性。也是说,浅拷贝中,原对象属性如果包含对象(假设A),则新对象会与原对象共享A的内存地址。但深拷贝中,原对象把所有嵌套属性的内存地址都拷贝一份给新对象。

所以,浅拷贝与深拷贝最主要的区别是,对象的内层(嵌套)属性有没有完全彻底拷贝,直到属性值为基本数据类型为止。

Jquery 里的浅拷贝和深拷贝

Jquery 作为最常用的工具库,它封装了一个扩展对象的方法,即 jQuery.extend([deep], target, object1 [, objectN]),通过它,我们可以轻松的实现浅、深拷贝:

var person1 = {company: 'xx', contact: {tel: 1234, title: '...'}},
person2 = {};

// 浅拷贝
person2 = $.extend({}, person1);

// 深拷贝
person2 = $.extend(true, {}, person1);

另外,underscore 好像只实现了 浅拷贝。

最后要说的是,所谓的 浅拷贝 和 深拷贝,只是针对引用数据类型而言。