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. 基本类型值有哪些?引用类型值有哪些?它们各有什么特点?