JavaScript 的数据类型分为基本类型值和引用类型值两种,两种数据类型在变量的传值时有较大不同。比

如,对于基本类型值:

var a = 3;

var b = a;

内存中产生了两个数字 3,变量 a 和变量 b 是完全分开的,我们实现了变量的“克隆”。“克隆”来源于生

物学术语,表示创造一个和现有元素相同的元素。

但引用类型值并不能轻松实现克隆,比如:

var a = [1,2,3];

var b = a;

内存中并没有生成一份数组的副本,变量 a 和变量 b 指向内存中的同一个数组。数组没有被克隆成功。如果

要克隆这个数组,须编写程序实现。

浅克隆

克隆一个数组算法比较简单:准备一个空的结果数组,然后遍历原数组,在结果数组中推入原数组的每一项

即可。代码如下:

function clone(arr){

var _arr = [];

for(var i = 0 ; i < arr.length ; i++){

_arr.push(arr[i]);

}

return _arr;

}

编写程序测试这个函数:

var list1 = [1, 2, 3,4 ];

var list2 = clone(list1); //克隆

console.log(list2); //[1, 2, 3, 4]

console.log(list1 == list2); //false

我们输出 list2 并且用==运算符检查 list1 和 list2 是否是内存中同一个对象,结果是 false,表示 list1

和 list2 不是内存中的同一个对象,数组克隆成功

list1 和 list2 是内存中的两个数组,但是我们仍然不能说两个数组已经“完全分开”,这是为什么呢?

我们试着克隆一个更为复杂的数组试试:

var list1 = [1, 2, 3, {"a" : 1}];

var list2 = clone(list1); // 克隆

console.log(list2); // [1, 2, 3, {"a" : 1}]

console.log(list1 == list2); // false

console.log(list1[3] == list2[3]); // true

这次测试将list1中存放了一个对象项,克隆得到list2数组。list2看起来是克隆成功的,用==比较list1

和 list2 结果也是 false,说明两个数组在内存是两个不同的数组。但问题出现在对两个数组下标为 3 的项的

==比较上,结果竟然是 true,说明这个{"a" : 1}对象在内存中仍然是同一个对象

这就说明:数组 list1 和 list2 是内存中两个不同的数组,但数组中的下标为 3 的这个对象{"a" : 1}仍

是内存中的同一个对象。

产生这个问题的原因也非常简单,通过查看 clone 函数的代码不难发现:clone 函数的机理是遍历每个元

素,将这个元素 push 到结果数组中。当遍历到基本类型值,系统会将它们在内存中产生一个副本,被推入结果

数组的实际上是这项的副本;但是当遍历到引用类型值的时候,系统不会将这个对象在内存中创造副本,而是直

接将原对象的引用推入结果数组。这就产生了“藕断丝连”的情况。

函数 clone 我们称为“浅克隆”(又称浅复制),它是一种“表层的”克隆形式。浅克隆的产物确实和被

克隆对象是两个不同的对象,但是它们并没有被完全分开。

实现深克隆

深克隆可以让两个引用类型值完全分开,其内部的引用类型值数据都会在内存中产生副本,不会有“藕断丝

连”的情况。

深克隆需要书写更复杂的程序来实现,需要使用递归思想。

比如克隆下面这个数组:

[1, 2, {a : 3, d : [{ e : 4}, 5]}, 6]

这个数组非常复杂,它的某个项是个对象,而对象的某个属性值又是个数组,这个数组又有项是个对象……。

可见要深克隆这个数组是一个“层层深入”的过程,我们不仅要克隆第一层,还要克隆第二层、第三层,直到克

隆到最内层的基本类型值元素。递归算法是解决这类型问题的最好的手段,我们只需要写出一层克隆的方法,然

后让程序不断深入迭代即可。

实现深克隆的代码如下:

function deepClone(o){

if(

typeof o == "string" ||

typeof o == "number" ||

typeof o == "undefined" ||

typeof o == "boolean"

){

return o;

}else if(Array.isArray(o)){

var _arr = [];

for(var i = 0 ; i < o.length ; i++){

_arr.push(deepClone(o[i]));

}

return _arr;

}else if(typeof o == "object"){

var _o = {};

for(var k in o){

_o[k] = deepClone(o[k])

}

return _o;

}

}

算法思想是:检测 o 的类型,如果 o 的类型是基本类型值,则返回 o 本身即完成了 o 的克隆;如果 o 是数

组,则遍历它的每一项,递归调用 deepClone 深克隆它的每一项;如果 o 是对象,则遍历它的每一个属性,递

归调用 deepClone 深克隆它的每一个属性值。

书写代码进行测试:

var arr1 = [1, 2, 3, {"a" : 1}];

var arr2 = deepClone(arr1); //深克隆

console.log(arr2); //[1, 2, 3, {"a" : 1}]

console.log(arr1 == arr2); //false

console.log(arr1[3] == arr2[3]); //false

和浅克隆不同,这次两个数组下标为 3 的项的相等比较结果是 false,说明在内存中{"a":4}已经是完全分

开的两个对象了。

本章作业题

1. 定义一个长度为 4 的数组,然后用程序删除它的末项、删除它的首项、在它的尾部插入一个新项、头部插入

一个新项。

2. 如何让数组["A", "B", "C"]变为["A", "C"]?

3. 如何让数组["A", "B", "C"]变为["M", "N", "A", "B", "C"]?你能用两种方法实现么?

4. 编写函数 sum(arr),返回数组 arr 中所有项的和

5. 实现数组的冒泡排序和二分排序,在纸上画出每种排序的执行过程图。请思考:二分排序法和冒泡排序哪个

效率更高?在什么情况下二分排序法的性能是最差的?在最差的情况,二分排序法和冒泡排序哪个更糟糕?

6. 编写程序求两个数组的交集、并集、差集。

7. 编写函数 maxItem(arr),返回 arr 数组中的最大值。

8. 编写函数 sample(arr, n),返回数组 arr 中的 n 项随机样本。

9. 编写函数 random(arr),返回将 arr 数组项随机打乱的新数组。

10. 编写函数 without(arr , value),返回一个删除所有 value 值的 arr 副本。

11. 递归算法是非常常见的算法思想,请归纳总结我们遇见的所有递归算法程序,并思考:在什么情况下需要使

用递归算法?

12. 编写一个二维数组,存储九九乘法表。

13. 基本类型值有哪些?引用类型值有哪些?它们各有什么特点?