第二章 数组
作者: Loiane Groner
数组是最简单的数据结构。因此,所有的编程语言都有内置的数组存储结构。JavaScript当然也有内置的数组结构。在这一章,我们将深入了解数组的特点与性能。
数组用于存储一系列类型相同的数据。虽然JavaScript允许我们生成存储不同类型的数组,但我们还是要遵循最佳实践,假设数组只能存储同类型的数据来构造数组。
为什么我们要使用数组? 假设我们要存储所在城市每个月的平均气温,我们应该会按照如下规范存储数据:
var averageTempJan = 31.9;
var averageTempFeb = 35.3;
var averageTempMar = 42.4;
var averageTempApr = 52;
var averageTempMay = 60.8;
然而,这并不是最好的方法。如果我们存储的是一年的气温,我们需要声明12个变量。如果需要存储的不仅仅是一年的气温,该如何处理呢?幸运的是,数组就是为这一应用场景产生的。我们可以用更简单的方法描述刚刚的数据:
averageTemp[0] = 31.9
averageTemp[1] = 35.3;
averageTemp[2]= 42.4;
averageTemp[3] = 52;
averageTemp[0] = 60.8;
当然我们也可以如下图展示averageTemp变量
生成并初始化一个数组
用JavaScript声明、生成并初始化一个数组是很简单的,如下:
var daysOfWeek = new Array( );//{1}
var daysOfWeek = new Array(7);//{2}
var daysOfWeek = new Array(‘Sunday’,’Monday’,‘Tuesday’,‘Wednesday’,‘Thursday’,‘Friday’,‘Saturday’); //{3}
我们可以通过一个new 关键字声明并生成一个新的数组 (行{1})。我们也可用 new关键字生成一个有特定长度的数组 (行 {2})。也可以用第三种方法直接将数组元素传入构造函数 (行 {3})。
但是,使用 new 关键字并不是最推荐的做法。如果你想用JavaScript生成一个数组,可以用简单的方括号完成,如下:
var daysOfWeek = [];
我们也可以在初始化数组时加入一些元素,如下:
var daysOfWeek = [ Sunday’,’Monday’,‘Tuesday’,‘Wednesday’,‘Thursday’,‘Friday’,‘Saturday’];
如果我们想知道数组里有多少个元素,我们可以使用length 属性。下面代码的输出将为7。
console.log(daysOfWeek.length);
为了访问特定位置的数组,我们也可以使用方括号,通过在方括号输入元素的下标即可。比如说,如果我们想知道数组里所有元素的值,我们需要遍历这个数组的元素并打印这些元素,如下:
for ( var i = 0 ; i< daysOfWeek.length ; i++){
console.log(daysOfWeek[i]);
}
让我们看看另一例子。如果我们想得到斐波那契数列的前20项,已知第一个是1,第二个是2,后面的数项是前两项之和,则代码如下:
var fibonacci = []; //{1}
fibonacci[1] = 1;
fibonacci[2] = 2;
for( var i = 3; i < 20; i++){
fibonacci[i] = fibonacci[i-1] + fibonacci[i-2]; ////{4}
}
for( var i = 1; fibonacci.length; i++){
console.log(fibonacci[i]);
}
在 行{1} ,我们声明并生成了一个数组。在 行 {2} 和 行 {3},我们把斐波那契数列的第一项和第二项赋值给了数组的第二项和第三项(在JavaScript中,数组的第一个元素的下标为0,但斐波那契数列并没有第零项,所以我们跳过了这一步)。
之后,我们要做的便是生成该数列的第三项至第二十项。为了达到这一目标,我们可以用一个循环把前两项之和赋值给当前值(行 {4}-从数组的第三个元素至第十九个元素)。
之后,我们就可以看见生成的结果了 (行 {6}),我们只要从数组的第一位循环至其长度即可 (行 {5})。
增加与删除元素
从数组删除或添加一个元素并不难,但有在添加和删除元素时有时会产生一些混乱。假设我们有一个从0到9的数字数组:
var numbers = [ 0,1,2,3,4,5,6,7,8,9] ;
如果我们要给这个数组增加一个元素,我们索引最后一个没有值的位置并添加值即可:
numbers[numbers.length] = 10;
note
在JavaScript中,数组是一个可变对象,所以我们可以很轻松的为之添加元素。只要我们给数组添加元素,这个数组对象就会动态的随之扩大。在很多其他语言中,如C 和 Java,我们需要决定数组的大小,如果我们需要添加更多元素至数组,我们需要产生一个全新的数组;我们无法随意地添加元素至数组。
然而,还有一种叫push的方法可以将新的元素放在数组的末尾。通过push方法我们可以添加很多元素进去:
numbers.push(11);
numbers.push(12,13);
这个数组的输出结果将会是从0到13的数字。
那么,如果我们想把新的元素添加到数组顶端,该如何做呢?首先,我们可以把数组所有的元素往右边移一个位置,好为第一个位置空出空位。我们可以从数组的最后一位遍历所有的元素,让i位的元素被赋予i-1位元素的值,以空出第0个位置,并为之赋予我们想添加的值-1:
for (var i = numbers.length; i >=0 ;i--){
numbers[i] = numbers[i-1];
}
numbers[0] = -1;
我们可以通过下面的图示展示这个过程:
JavaScript 的array对象还有一个叫unshift的方法,可以实现把元素添加在数组的顶端:
numbers.unshift(-2);
numbers.unshift(-4,-3);
通过使用unshift方法,我们可以吧-2、-3、-4这些值添加到数组的顶部。这个数组就成为了从-4到13的数字数组。
目前为止,我们已经学习了如何给数组添加一个元素,现在我们看看如何给数组删除一个元素。
我们可以使用pop方法:
numbers.pop();
tip
push 和 pop 方法可以实现 stack数据结构,当然这是下一章的内容了。
这个数组是一个从-4到12的数组,它的长度是17。
如果想要从数组的顶部删除数据,我们可以用下面的代码实现。
for ( var i = 0; i<numbers.length; i++){
numbers[i] = numbers[i+1]
}
这是上面的代码的图示:
我们把所有的元素往左移动了一个单位。然而,数组的长度并没有改变,这意味着我们的数组还有一个多余的元素(值为undefined)。循环执行到最后一个元素时,被赋值的值是一个不存在的值(在有的语言中,这样是会报错的)。
正如我们所见,这样的方法只能删除一个值,但我们未能真正的删除一个元素(因为数组的长度并没有变,还有一个值为undefined 的元素)。
为了真正意义上地从数组的顶部删除一个元素,我们可以使用shift方法,如下:
numbers.shift();
现在,这个数组就是一个从-3到12、长度为16的数组了。
tip
shift 和 unshift方法可以用于实现一个队列数组结构,当然了,这是第四章的内容。
现在,我们已经知道如何从数组的顶部删除和添加一个元素,也知道了如何从数组的尾部删除和添加一个元素,那如果我们想要从数组的特定位置删除一个元素,应该如何操作呢?
我们可以使用splice方法,在调用函数时声明要开始删除的位置和要往后删除多少个元素即可:
numbers.splice(5,3);
执行了这段代码后,机器将会从数组下标第五位开始,删除三个元素,这意味着 numbers[5], numbers[6], numbers[7],将会从数组中被删除,该数组剩下的的元素则为 -3,-2,-1,0,5,6,7,8,9,10,11,12(因为 2, 3, 4已经被删除了)
如果,我们想把2、3、4三个元素放回数组,我们可以这样使用splice方法:
numbers.splice(5,0,2,3,4);
第一个参数是我们想要删除元素或者插入元素时的起点。第二个参数是我们要删除元素的数量(如果我们不想删除其中元素,我们输入0即可),第三个及以后的参数则是我们想要插入的元素(如2,3和4)。这个数组则会变回成为从-3到12的数字数组。
最后,我们执行以下的代码:
numbers.splice(5,3,2,3,4);
数组的结果是从-3到12.这是因为从第五个下标删除了三个元素,之后又从第五个下标添加进了2、3和4。
合并多个数组
设想这样一个场景:有多个不同的数组需要你放入到一个数组里面。我们可以遍历数组的每一个元素再把他们放在结果数组里面。幸运的是,JavaScript已经内置了一个concat 方法为我们做了这件事:
var zero = 0;
var positiveNumbers = [1,2,3];
var negativeNumbers = [-3,-2,-1];
var numbers = negativeNumbers.concat(zero, positiveNumbers);
我们可以在concat方法里面放入尽可能多的数组。它们最后会被按照放入的顺序排列进去。在上面的例子中,negativeNumbers数组后面的是zero数组的元素,再后面是positiveNumbers数组的元素。这个数组的结果是一个由-3、-2、-1、0、1、2、和3组成的数字数组。
遍历函数
有时我们需要遍历数组里的元素。我们已经学过使用for循环来实现这一功能。 JavaScript还有内置的数组遍历方法。这一段节的例子将会用到一个数组和一个函数。我们会用到一个从1到15的数组,和一个判断一个数字是否可以被二整除的函数:
var isEven = function(x){
console.log(x);
return (x % 2 === 0) ? true : false;
}
var numbers = [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15];
下面第一个要介绍到的是every方法。every方法会遍历数组里每一个元素,直到every里面的函数的返回值为false时退出:
numbers.every(isEven);
在这个例子中,数组的第一个元素是1,而1不能被2整除,所以isEven函数的返回值为false,而这也是isEven函数唯一一次被执行。
接下来,我们要介绍的是some方法。some方法也会遍历每一个元素,它与every方法不同的是,它会遍历到有一个函数的返回值为true时停止:
numbers.some(isEven);
在这个例子中,数组里的第一个偶数是2。第一个被遍历的元素是1,函数会返回false。然后,第二个被遍历的元素是2,函数会返回true—遍历也在此结束。
如果我们想按照我们的意愿来遍历数组,我们可以使用forEach方法。这个方法的输出结果和使用循环是一样的:
numbers.forEach( function(x) ){
console.log((x % 2 === 0));
}
JavaScript还有另外两个会返回新数组的遍历方法。第一个是map方法:
var myMap = numbers.map(isEven);
myMap数组会有这些值:[false, true, false, true, false, true, false, true, false, true]
它会存储map方法里的函数的返回值。通过这个方法,我们可以快速的知道一个值是否为偶数。比如说,number[0]是fasle因为1为单数,number[1]为true是因为2为偶数。
另一个是filter方法,它会返回一个值为满足条件函数的数组:
var evenNumbers = numbers.filter(isEven);
在这个例子中,evenNumbers是一个值为[2,4,6,8,10,12,14]的数组。
最后一个要介绍到的是reduce方法。reduce 方法接受了一个包含previous、current、index三个参数的函数。我们可以用这个方法作出一个数组累加器:
numbers.reduce(function(previous,current,index){
return previous+current;
}
这个函数的执行结果为120。
搜索方法与分类方法
在本书中,我们将学习如何写最常用的搜索算法和排序算法。然而,JavaScript有内置的排序函数,和一系列搜索函数。让我来了解一下这些函数。
首先,假设我们有一个排好顺序的数组numbers(1,2,3,…15),如果要让这个数组的顺序倒过来,我们可以使用reverse函数,这个函数可以使数组的最后一位变成最前一位、最前一位变成最后一位:
numbers.reverse();
那么现在数组的顺序为: [15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1]。之后,我们可以使用sort方法:
numbers.sort();
然而,这个数组的结果确是[1, 10, 11, 12, 13, 14, 15, 2, 3, 4, 5, 6, 7, 8, 9]。数组却并没有按照升序或降序排列。这是因为sort方法把numbers数组成员当做字符串、按照词典顺序进行排列了。
我们可以自己写一个比较函,鉴于数组的成员都是数字,我们可以用这样的比较函数:
numbers.sort(function(a,b){
return a-b;
});
如果b大于a,这段代码将会返回一个负数;如果b小于a,这段代码将会返回一个正数;如果b等于a,则返回0.这意味着,如果返回值为负数,a小于b,而这是sort函数进一步分类的依据。
刚刚的代码可以改写成这样:
function compare(a,b){
if (a<b){
return -1;
}
if (a>b){
return 1;
}
return 0;
}
numbers.sort(compare);
compare函数可以作为sort方法的参数传入,因为JavaScript 中的 Array数据类型的sort方法可以接受一个负责排序的函数作为参数。在上述的例子中,我们声明了一个将数组进行升序排列的比较函数。
自定义排序
我们可以声明一个比较函数,给成员为对象的数组进行排序。比如说,我们有若干由age 和 name 属性组成的对象,而这些对象都是数组的成员,我们相对这些对象依据age属性进行排序,我们可以使用下面的代码:
var friends = [ {name: 'John', age:30}, {name: 'Ana', age:20}, {name: 'Chris', age:30}];
function comparePerson(a, b){
if (a.age < b.age){
return -1;
}
if (a.age > b.age){
return 1;
}
return 0;
}
console.log(friends.sort(comparePerson));
在这个例子中,friends排序后的结果为 Ana(20), Chirs(25), John(30)。
对字符串进行排序
假设我们有这样一个数组:
var names = ['Ana', 'ana', 'john', 'John'];
console.log(names.sort));
这段代码在控制台的输出结果如下:
['Ana','John','ana','john'];
既然字母a 是字母表的第一个字母,为什么ana 在 John的后面呢?这是因为JavaScript是根据每个字符串的ASCII码,对字符串进行排序的。比如说,A,J,a 和 j的ASCII码 分别为 65,74,97,106。
因此,J的ASCII值是低于a的,所以'John'排在了 'ana'的前面。
note
如果想进一步了解ASCTT表,可以参考 www.asciitable.com/。
现在如果我们想实现忽视字母大小写进行排序,我们可以使用这样的代码:
numbers.sort(function(a, b){
if (a.toLowerCase() < b.toLowerCase() ){
return -1;
}
if (a.toLowerCase() > b.toLowerCase() ){
return 1;
}
return 0;
});
这时数组的排序结果为[ 'Ana', 'ana', 'John','john'];
对于有音调的字符串,我们也可以使用localeCompare函数:
var names2 = ['Mae´ve', 'Maeve'];
console.log(names2.sort(function(a,b){
return a.localeCompare(b);
}));
此时代码的输出结果为 ['Maeve', 'Mae´ve']。
搜索方法
JavaScript为我们提供了两种搜索方法:indexOf 和 lastIndexOf。IndexOf会为我们返回第一符合条件的成员的下标;lastIndexOf会为我们返回最后第一符合条件的成员的下标;让我对刚刚的numbers数组使用这两个方法:
console.log(numbers.indexOf(10));
console.log(numbers.indexOf(100));
在这个例子中,第一行代码的输出结果为9,而第二行代码的输出结果为 -1(因为numbers数组中没有100)。
以下为调用lastIndexOf方法的代码
numbers.push(10);
console.log(numbers.lastIndexOf(10));
console.log(numbers.lastIndexOf(100));
我们给numbers数组增加了一个值为10的成员,所以第二行代码的输出值为15,而第三行代码的输出结果为-1(因为numbers数组中没有100)。
将数组成员转为字符串
最后,我们来看看最后两个数组方法: toStirng 和 join方法。
如果我们想要把数组所有成员以一个字符串的形式输出,我们可以使用toString方法:
console.log(numbers.toString() );
该代码的输出结果为值为"1,2,3,4,5,6,7,8"的字符串。
如果我们想使用符合对数组成员进行分割,我们可以使用join方法:
var numbersString = numbers.join('-');
console.log(numbersString);
数组的输出结果则为:
“1-2-3-4-5-6-7-8-9-10-11-12-13-14-15-10”
小结
在这一章,我们讲解了最常用的数据结构:数组。我们已经学了如何声明、初始化一个数组,如何给一个数组赋值与删除值。
在下一章,我们将学习特殊形式的数组—栈。
注:本文翻译自Loiane Groner的《Learning JavaScript Data Structures and Algorithm》