数组,是一段线性分配的,具有非常高性能的数据结构。简单地说,数组以连续的空间存储,通过整数地计算偏移量访问其中的元素,将读取修改的时间复杂度降低至O(1),我们称之为猝发式存取。是不是非常期待?没错,像这样的好东西,JavaScript没有。
1. Array简介
但作为替代,JavaScript设计者想出了一个更方便但性能相对较低的方案,打印观察Array.prototype,会发现,设计者为我们提供的是一个array-like(类数组)的对象。在检索和更新属性上,Array就和普通的对象一模一样(也就是说要遍历所有属性),只是多了一个不可枚举的属性length来记录这个对象所表示的数组的长度。尽管Array对象的性能明显比数组要差,但是搭配上弱类型的JavaScript语言(当然,JavaScript中不存在传统的数组一部分原因也是因为这一点),它在使用上非常的方便。更贴心的是,设计者还为我们提供了许多内置的方法,可以快速解决其他语言费很大劲才能解决的问题。
2. 声明
数组的声明跟对象的声明很类似。我们可以用两种方法初始化一个数组:① 直接用 var array = []; 我们称之为数组字面量的方式来初始化;② 使用构造函数 var array = new Array(); 如果参数填入一个数字,则返回一个长度为这个数字的空数组,如果参数填入多个值,则返回一个按顺序保存了这些值的Array对象。
3. 修改
上面我们已经搞清楚了Array对象总体的结构,这样修改一个数组就可以转化为我们以前学到的修改一个对象属性的知识了。由于JavaScript的灵活性,除非你定义一个大到Infinity的数组,其他情况下均不会因为越界报错(Runtime Error也许是很多人的噩梦,反正是我的噩梦)。因此,假如我们现在有这样一个数组:
1 var myArray = [0, '1', true]
利用JavaScript会帮我们维护length属性这一特点(但为了编写易维护的代码,不推荐这些简单粗暴的做法),我们也可以做一些在别的语言中看起来不可思议的操作,例如:① myArray[8] = undefined 会直接把Array的长度扩展到8;② myArray.length = 0; 能直接清空数组。除了可以这样清空数组,还可以通过Array.prototype.splice方法完成清空,因此我们把目光放到数组的原型方法上来。
4. 原型方法(均以上面的myArray举例)
① indexOf
indexOf方法同时存在于Array.prototype和String.prototype中,可以用它来检测数组或字符串中是否存在对应的元素,如果存在,则返回它的下标,如果不存在,则返回-1:
1 console.log(myArray.indexOf(0)); //0 2 console.log(myArray.indexOf(1)); //-1
由于在数组中,1和'1'是两个不同的值,因此第二句返回结果-1。
② push、pop、unshift、shift
push朝数组末尾推入若干新元素,返回加入后数组的length。
pop弹出数组末尾的一个元素,返回被弹出的元素。
unshift朝数组开头推入若干新元素,返回加入后数组的length。
shift弹出数组开头的一个元素,返回被弹出的元素。
1 console.log(myArray.push('A', 'B')); //5 2 console.log(myArray.pop()); //'B' 3 console.log(myArray.unshift('A', 'B')); //6 4 console.log(myArray.shift()); //'A'
③ sort(这时候我们重新定义一下 myArray = [5, 2, 0, 10, 17, 25]; )
sort方法在原数组上动刀,这里我们期望将数组中的数按从小到大的顺序排列,sort函数可以帮我们做到这一点,但需要注意的是,sort函数默认把这些元素转化为字符串进行比较。因此这个数组排序以后的结果是这样的:
1 console.log(myArray.sort()); // [0, 10, 17, 2, 25, 5]
因此通常需要填入一个比较判断函数作为参数,下面传入一个箭头函数,按照我所定义的这个函数进行判断大小再排序:
1 console.log(myArray.sort((x, y)=>{return x - y;})); // [0, 2, 5, 10, 17, 25]
④ reverse和join
reverse方法在原数组上动刀,返回跟原来相反的数组;join方法相当于String.prototype.split方法的反函数,填入一个字符串参数,以这个参数将每个元素分隔开,合并成一个字符串并返回。这两个方法可以搭配使用来处理反向输出字符串的问题:
1 var str = "Hello world!"; 2 3 console.log(str.split('').reverse().join('')); 4 //"!dlrow olleH" 5 console.log(str.split(' ').reverse().join(' ')); 6 //"world! Hello"
这里由于每个函数返回值都是与其相对应的数组或字符串,可以直接在这个返回值上进行操作,因此我们还用到了链式调用的技巧。
⑤ slice和splice
这两个方法的区别和使用非常重要,又由于它们名字之间只差一个字母,缺少练习时我们很容易会将其混淆。
slice方法可以类比String.prototype.substring。指定一个参数n,它将返回一个新数组,这个数组中含有原数组下标从n到末尾的所有元素。指定两个参数a和b时,它将返回一个新数组,这个数组含有原数组下标从a到b的所有元素(不得不说,用中文来描述真的非常蹩脚):
1 var myArray = [1, 2, 3, 4, 5, 6, 7, 8]; 2 3 var aNewArray = myArray.slice(3); 4 var aNewNewArray = myArray.slice(3, 5); 5 6 console.log(aNewArray); 7 //[4, 5, 6, 7, 8] 8 console.log(aNewNewArray); 9 //[4, 5]
当然了,这两种操作都是含头不含尾的。如果不指定参数地使用slice,它将返回一个跟原数组一模一样的数组,利用这一点,我们可以用一句代码复制一个数组。
splice是修改一个数组的“万能方法”,要注意它将直接在原数组上动刀,返回值是被删除的元素组成的数组:
1 var myArray = ['CapAmerica', 'IronMan', 'Hulk', 'Thor']; 2 // param: 从第4个元素开始操作,删除0个元素,加入新元素'BlackWidow' 3 myArray.splice(3, 0, 'BlackWidow'); 4 // 由于删除0个元素,它将返回一个空数组 5 6 console.log(myArray); 7 // ['CapAmerica', 'IronMan', 'Hulk', 'Thor', 'BlackWidow'] 8 9 // param:从第二个元素开始,删除三个元素,加入这些新元素 10 myArray.splice(2, 3, 'ScarletWitch', 'Vision', 'CapMarvel'); 11 //返回['Hulk', 'Thor', 'BlackWidow'] 12 13 console.log(myArray); 14 //['CapAmerica', 'IronMan', 'ScarletWitch', 'Vision', 'CapMarvel']
⑥ concat
concat方法不会动原数组,而是返回新数组。它返回一个将原数组和所有你填入的参数都合并在一起的新数组,因此我们想到了可以用它搭配splice方法来实现一个JavaScript版本的快速排序:
1 function quickSort(arr) { 2 if(arr.length <=1) return arr; 3 var pivotIndex = Math.floor(arr.length / 2); 4 var pivot = arr.splice(pivotIndex, 1)[0]; 5 var left = [], right = []; 6 for(var i = 0; i < arr.length; i++) { 7 if(arr[i] < pivot) left.push(arr[i]); 8 else right.push(arr[i]); 9 }; 10 return quickSort(left).concat(pivot, quickSort(right)); 11 }
拓展:1. ES6 Map
我们都知道,在Array对象中,每个元素就是一个对象,通过键值对存储数据。但其中的键只能是字符串类型。我们渴求能用类似Number或其他基本数据类型作为键,这样的表达会更清晰。因此ES6为我们带来了Map数据结构,它还具有极高的查询效率。且看它的使用语法:
1 var myMap = new Map([['name', 'MotherLyn'],['score', 51]]);
观察发现,我们完全可以把它当做是一个两列n行的,对数据类型有规范的二维Array。它还有许多内置方法:set、get、has、delete。
要注意的是,Map中所有的键类似数据库中的primary key,也就是他们都是不可重复的,填入相同的键不同的值只会把以往的数据冲掉。
2. ES6 Set
Set与Map类似但又十分不同。说它相同是因为Set对数据类型也是存在划分的(3和‘3’是不同的两个键);说它不同是因为Set只一个值而不是一个键值对。因此使用Set时只需要传入一个一维的Array即可:
1 var mySet = new Set(['MotherLyn', 51]);
通过使用内置方法add、delete和has可以进行增删查改操作。
3. ES6 for of遍历方法
前面我们提到,使用for in遍历会将可枚举的原型属性一块遍历到。为了解决这个问题,ES6提出了for of遍历。它在使用上和for in是相同的,只是把in改成of而已。
总结:① JavaScript中的Array对象只是一个内建的对象,并不是传统意义上的数组,它在内存中不是连续的空间,因此只能遍历元素来进行查找修改,性能较差但灵活性非常好。
② Array.prototype中有非常多的方法,常用的有以上这些:indexOf、push、pop、unshift、shift、sort、reverse、join、slice、splice和concat。还有很多其他的方法,要善用这些方法只能靠多练习,慢慢积累。
③ ES6中的Map和Set都是类似Array的数据结构,它们都严格管理数据类型,但区别是Map以键值对的方式存储,Set只能存储值而不能存储键。