作为ECMAScript最常用的数据类型之一,数组(Array)的作用越来越重要,功能也是越来越强大。在ES6之后,我们能对数组进行更多、更复杂的操作。本着方便查阅和分享的初衷,写下了这篇文章,希望对大家有所帮助。
如何创建一个数组?JavaScript有着传统的两种方式:Array构造函数定义 与 字面量定义,在这之后,ES6又新增了两种创建数组的方式:from() 方法 与 of() 方法。
下面我们一起来了解JavaScript给我们提供的这四种创建数组的方式:
1. 使用Array构造函数来创建数组
使用Array构造函数创建数组,只需调用new关键字:
let arr = new Array();
此时我们便创建了一个内容为空的数组,打印arr,我们发现结果是一对中括号[],里面并没有任何内容。
接下来我们会给arr写入参数,写入参数的不同会导致许多意料之外的结果,我们分别来看一下:
只传入一个数字
let arr = new Array(5); //new 可以省略,结果相同,如:let arr1 = Array(5);
此时我们只向arr中参入一个数字,打印结果,我们会看到数组长度(arr.length)为5,内容为5个空值的数组:
此时我们并没有得到一个数组长度为1,且数组内容为数字5的数组,而是得到了数组长度是5,内容皆为empty(空值)的数组,所以,如果用Array构造函数的方式来创建数组,并且只传入一个数字,那么就代表了该数组占有了一些空槽位,空槽位的个数取决于数字的值。
【注意】我们无法通过Array构造函数的方式来创建一个只有一个元素,且该元素为数字的字符串。这也是Array构造函数创建数组的弊端。
传入多个值
JavaScript中的数组与其他语言有所不同,数组的每个槽位可以存放任意类型的数据。也就是说,我们可以创建一个数组,它的第一个值可以是字符串,第二个值可以是数字,第三个值可以是一个对象……这点和其他语言有所不同。
let arr = new Array("my age is",18,{age : 18});
打印结果,成功创建一个长度为3,储存的值类型不同的数组:
2. 使用字面量形式来创建数组
我们知道,创建数组之后,数组的呈现方式是一个中括号[],里面包含着我们存储的值,并用逗号分隔开。同样,我们也可以手动将这个数组的形式写出来,从而创建一个数组:
let arr = ["height is",10000,{meter:10000}]; let arr1 = [];//字面量形式定义空数组 let arr2 = [1,2,];//最后一个值后面加逗号,并不影响数组的值(打印结果为[1,2]) let arr3 = [5];//字面量形式成功避免了构造函数创建数组中的弊端(打印结果为[5])
分别打印,结果如下:
3. 使用from()静态方法来创建数组
ES6新增了from()静态方法,它主要用于将类数结构转换为数组实列。
所谓类数组对象,即任何可以迭代的结构(prototype中含有Symbol.iterator属性),或者有一个length属性和可索引元素的结构。而Array.from()方法的第一个参数就是一个类数组对象,它可以用于很多场合:
将字符串分隔成单字符数组
console.log(Array.from("JavaScript"));//["J","a","v","a","S","c","r","i","p","t"]
使用from将集合和映射转换为一个新数组
const m = new Map().set(1,2) set(3,4); const s = new Set().add(1), .add(2), .add(3), .add(4); console.log(Array.from(m));//[[1,2],[3,4]] console.log(Array.from(s));//[1,2,3,4]
对现有数组执行浅复制
const a1 = [1,2,3,4]; const a2 = Array.from(a1); console.log(a1);//[1,2,3,4] console.log(a1 === a2);// fasle
可以使用任何可迭代对象
const iter = { *[Symbol.iterator](){ yield 1; yield 2; yield 3; yield 4; } }; console.log(Array.from(iter));//[1,2,3,4]
轻松将arguments对象转换为数组
function getArgsArray(){ return Array.from(arguments); } console.log(getArgsArray(1,2,3,4)); // [1,2,3,4]
转换自定义对象
const arrayLikeObject = { 0 : 1 , 1 : 2 , 2 : 3 , 3 : 4 , length : 4 }; console.log(Array.from(arrayLikeObject)); // [1,2,3,4]
Array.from()方法还有第二个参数,它是一个可选的映射函数参数。这个函数可以直接增强新数组的值,而无需想调用Array.from().map()那样先创建一个中间数组。同时,它还有第三个参数,用于指定指定映射函数中this的值。注意,这个重写的this值在不能用在箭头函数中。
const a1 = [1,2,3,4]; const a2 = Array.from(a1,x => x**2); const a3 = Array.from(a1,function(x) {return x**this.exponent},{exponent: 2});//第三个参数确定了this的值 console.log(a2);//[1,4,9,16] console.log(a3);//[1,4,9,16]
4. 使用of()静态方法来创建数组
Array.of()静态方法使用频率并不高,它主要用于将一组参数转换为数组。这个方法最常用的地方是用于代替在ES6之前我们经常使用的Array.prototype.slice.call(arguments)方法:
console.log(Array.of(1,2,3,4)); // [1,2,3,4] console.log(Array.of(undefined)); // [undefined]
【小结】关于创建数组,from()方法和of()方法更加类似于将一个非数组或类数组的值转换成一个数组,而并非像前两种创建数组的方法那样方便地直接写入我们自己定义的值。而且这两种方法操作比较复杂,对于初学者来说,先掌握前两种创建数组的方式,在之后的JS学习中慢慢掌握from()和of()方法不失为一种好的方式。
当我们使用字面量来创建数组时,用两个逗号可以在数组中创建一个空位(hole),在ES6之后,JS会将这些空位当成一个存在的元素,而这些元素的值为undefined。
const options = [1,,,,5]; console.log(options.length);//结果为5,即数组长度为5 console.log(options);//输出结果:[1,empty,empty,empty,5] console.log(options[1]);//输出结果:undefined
数组索引一般有两个功能:
注意:数组中的索引是从0开始计数的
在中括号中提供的索引表示要访问的值。如果索引小于数组包含的元素数,则返回存储在相应位置的元素,例如:
let array = ['apple','ball','color']; console.log(array[0]);//apple console.log(array[1]);//ball console.log(array[2]);//color console.log(array[3]);//undefined
输出结果:
注意,如果提供的索引不小于数组包含的元素数。则返回undefined。
设置数组值的方法也是与获取数组值一样,就是替换指定位置的值。如果把一个值设置给超过数组最大索引的索引,则数组长度会自动扩展到该索引值加1.
let array = ['apple','ball','color']; console.log(array.length)//添加元素前的数组长度,长度为3 array[3] = 'dog'; console.log(array[3]);//dog console.log(array.length)//添加元素后的数组长度,长度为4
输出结果:
在数组中,还有一个重要的属性:length
数组中的元素的数量保存在length属性中,即数组长度。这个属性始终返回0或大于0的值
let arr = []; console.log(arr.length);//0
数组length属性的独特之处在于,它不是只读的。通过修改length属性,可以从数组末尾删除或添加元素。例如:
let array = ['apple','ball','color']; console.log(array[2]);//此时输出结果为'color' array.length = 2; console.log(array[2]);//此时输出结果为undefined
输出结果:
反之,如果将length设置为大于数组元素数的值,则新添加的元素都将以undefined填充,例如:
let array = ['apple','ball','color']; array.length = 4; console.log(array[3]);//undefined
另外,如果array中若有一个值被插入一个很大的位置,例如位置99,此时新length就变成了100(99+1)。这中间的所有元素,即位置3-98,实际上并不存在,因此在返回时会返回undefined。
let array = ['apple','ball','color']; array[99] = 'something'; console.log(array.length);//100 console.log(array[50]);//undefined
【注意】数组最多可以包含4 294 967 295个元素,如果尝试添加更多项,则会导致抛出错误。若以这个最大值作为初始化数组,可能导致脚本运行时间过长的错误。
JS中的一个经典问题就是判断一个对象是不是数组。在只有一个网页(只有一个全局作用域)的情况下,使用instanceof就可以判断:
if(value instanceof Array){ //操作数组 }
使用instanceof的问题是假定只有一个全局执行上下文。如果网页里有多个框架,则可能设计两个不同的全局执行上下文,因此就会有两个不同版本的Array构造函数。如果要把数组从一个框架传给另一个框架,则这个数组的构造函数将有别于第二个框架内本地创建的数组。
为解决这个问题,ECMScript提供了Array.isArray()方法。这个方法的目的就是确定一个值是否为数组,而不用管它是在哪个全局执行上下文中创建的。
if(Array.isArray(val)){ //操作数组 }
在ES6中,Array的原型上暴露了3个可以用于检索数组内容的方法,他们分别是:
keys()
用于返回数组上索引的迭代器valuse()
用于返回数组上元素的迭代器entries()
用于返回“索引/值”对的迭代器我们来看一下这几个方法的作用,我们新建一个数组a,然后使用这几个方法,并在控制台中打印:
const a = ['apple','ball','color','dog']; console.log(a.keys()); console.log(a.values()); console.log(a.entries());
输出结果:
结果和我们想象的并不同,从图中我们也可以得知,因为这三个方法返回的都是迭代器,索引我们无法直接得到我们想要的结果。所以我们在这里补充一个方法:
【补充】:Array.from()方法。它可以直接将形似数组的对象转换成数组实例,例如本例中的迭代器,如果我们加上Array.from()方法:
const a = ['apple','ball','color','dog']; console.log(Array.from(a.keys())); console.log(Array.from(a.values())); console.log(Array.from(a.entries()));
再来看一次结果,我们发现这次成功输出我们想要的结果了:
这两个方法:批量复制方法fill()
和填充数组方法copyWithin()
,都是ES6新增的方法。这两个方法都需要指定既有数组实例上的一个范围,包含开始索引,不包含结束索引。使用这个方法创建的数组不能缩放。
fill()方法
下面演示几种fill()
方法使用场景:
//初始化数组 const arr = [0,0,0,0,0]; //用5填充整个数组 arr.fill(5); console.log(arr);//[5,5,5,5,5] arr.fill(0);//重置,重置为[0,0,0,0,0] //用6填充索引大于等于3的元素 arr.fill(6,3); console.log(arr);//[0,0,0,6,6] arr.fill(0);//重置 //用7填充索引大于等于1且小于3的元素 arr.fill(7,1,3); console.log(arr);//[0,7,7,0,0] arr.fill(0);//重置 //用8填充元素大于等于1且小于4的元素 arr.fill(8,-4,-1); console.log(arr);//[0,8,8,8,0]
控制台结果:
【补充】:fill()
方法静默忽略超出数组边界、零长度以及方向相反的索引范围。
copyWithin()方法
copyWithin()
方法会按照指定范围浅复制数组中的部分内容,然后将其插入到指定索引开始的位置。fill()
方法里的相同。copyWithin()
方法使用场景://初始化数组 let arr = [0,1,2,3,4,5,6,7,8,9]; //从arr中复制索引0开始的内容,插入到索引5开始的位置 //在原索引或目标索引到达数组边界时停止 console.log(arr.copyWithin(5));//[0,1,2,3,4,0,1,2,3,4] arr = [0,1,2,3,4,5,6,7,8,9];//重置 //从arr中复制索引5开始的内容,插入到索引0开始的位置 console.log(arr.copyWithin(0,5));//[5,6,7,8,9,5,6,7,8,9] arr = [0,1,2,3,4,5,6,7,8,9];//重置 //从arr中复制索引0开始到索引3结束的内容 //插入索引4开始的位置 console.log(arr.copyWithin(4,0,3));//[0,1,2,3,0,1,2,7,8,9] arr = [0,1,2,3,4,5,6,7,8,9];//重置 //支持负值 console.log(arr.copyWithin(-4,-7,-3));//[0,1,2,3,4,5,3,4,5,6]
控制台结果:
【补充】:同样地,copyWithin()
方法静默忽略超出数组边界、零长度以及方向相反的索引范围。
所有的对象都有toString()方法和valueOf()方法。数组作为特殊的对象,同样拥有这两种方法,即使valueOf()方法返回的仍然是数组本身。如果你想知道valueOf()在对象中的用法,可查阅这篇文章
数组转字符串toString()(默认逗号分隔符)
toString()
方法会返回由数组中每个值的等效字符串拼接而成的一个逗号分隔的字符串:
let colors = ['red','blue','green']; console.log(colors.toString());//red,blue,green
打印结果:
【补充】:如果使用alert()
方法直接打印数组,则它会在底层中调用数组的toString()
方法,得到结果和前面相同。
let colors = ['red','blue','green']; alert(colors);//red,blue,green
数组转字符串join()(使用不同分隔符)
join()
方法接受一个参数,及字符串分隔符。返回包含所有项的字符串:(分别以 | 分隔和空格分隔)
let colors = ['red','blue','green']; console.log(colors.join('|'));//red|blue|green console.log(colors.join(' '));//red blue green
输出结果:
【补充】:如果数组中任意一项是null
或undefined
,那么在join()
,toString()
,valueOf()
返回的结果中都会以空字符串来表示。
字符串转数组
通过使用字符串方法split()
来将字符串转换成数组。由于split()
方法属于字符串方法的范畴,这里就不再叙述。
栈是一种后进先出(LIFO,Last-In-Firtst-Out)的数据结构,即最后添加的项先被删除。数据项的插入(push)和删除(pop)只会在栈顶发生。
类似地,ES6为数组添加了push()
和pop()
两种方法,来模仿栈的行为。
用push()方法从尾部添加数据
push()
方法接受任意数量的参数,并将它们添加到数组末尾,并返回数组的最新长度,可接受多个参数,多个参数用逗号隔开。
let array = ['apple','ball','color']; console.log(array.push('dog'));//4 console.log(array);//['apple','ball','color','dog']
打印结果,将数据添加到数组尾部:
用pop()方法从尾部删除数据
pop()
方法则用于删除数组的最后一项,同时减少数组的length值,返回被删除的项,不接受参数。
let array = ['apple','ball','color']; console.log(array.pop());//color console.log(array.length);//2 console.log(array);//['apple','ball']
打印结果,数据从数组尾部删除:
【注意】:两种方法的返回值不同,需要读者留意。
与栈方法不同,队列是一种先进先出(FIFO,First-In-Firtst-Out)的数据结构。即最先进入的数据先删除。队列在列表末尾添加数据,但要从列表开头获取数据。
上文中的push(
)可以解决数组末尾添加数据的问题,所以我们还要有种方法,从列表开头获取数据。
用shift()方法从首部删除数据
shift()
方法会删除数组第一项的值并返回它,不接受参数:
let array = ['apple','ball','color']; console.log(array.shift());//apple console.log(array.length);//2 console.log(array);//['ball','color']
输出结果,数据从数组首部删除:
用unshift()方法从首部添加数据
ES6也提供了在首部添加数据的方法:unshift()
。顾名思义,它可以在数组首部添加任意多个值,然后返回新的数组长度。可接受多个参数,多个参数用逗号隔开。
let array = ['apple','ball','color']; console.log(array.unshift('zero'));//4 console.log(array);//['zero','apple','ball','color']
打印结果,将数据添加到数组首部:
数组有两个方法可以用来对元素重新排序:reserse()
和sort()
reverse()方法实现数组元素反向排序
let values = [1,2,3,4,5]; console.log(values.reverse()); //5,4,3,2,1
输出结果:
更加灵活的sort()排序
默认情况下的sort()
排序,他会按照升序重新排列数组,即最小值在前面,最大值在后面:
let values = [2,6,8,3,5]; console.log(values.sort());//2,3,5,6,8
打印结果:
它的实现原理是在数组上的每一项都调用String()转型函数,然后比较字符串来决定顺序。
但是,当数组是这样的时候,意外就发生了:
let values = [0,1,5,10,15]; console.log(values.sort());//0,1,10,15,5
打印结果:
一开始数组中的顺序是正确的,但调用sort()方法会按照这些数值的字符串形式重新排序。即使5小于10,但字符串“10”在“5”之前,所以10还是排在5的前面。
为此,sort()
自身提供了一个解决方案,即接受一个比较函数,用于判断哪个值应该排在前面。
比较函数接受两个参数,如果第一个参数排在第二个参数前面,就返回负值;如果两个参数相等,就返回0;如果第一个参数应该排在第二个参数后面,就返回正值。下面举一个简单的例子:
function compare(value1,value2){ if (value1 < value2){ return -1; }else if(value1 > value2){ return 1; }else { return 0; } } let values = [0,1,5,10,15]; console.log(values.sort(compare));//0,1,5,10,15
打印结果:
对于数组种的元素,我们有三种强大的操作方法:concat(),slice()和splice()
使用concat()方法拼接数组
let colors = ['red','green','blue']; let colors2 = colors.concat('yellow',['black','brown']); console.log(color); // ['red','green','blue'] console.log(colors2); //['red','green','blue','yellow','black','brown']
打印结果:
使用slice()方法拆分数组
slice()
方法用于创建一个包含原有数组中一个或多个元素的新数组。slice()
方法可以接收一个或两个参数:返回元素的开始索引和结束索引。slice()
会返回该索引到数组末尾的所有元素。slice()
返回从开始索引到结束索引对应的所有元素,其中不包含结束索引对应的元素。slice()
方法不影响原始数组let colors = ['red','green','blue','yellow','purple']; let colors2 = colors.slice(1); let colors3 = colors.slice(1,4); console.log(colors);//'red','green','blue','yellow','purple' console.log(colors2);//'green','blue','yellow','purple' console.log(colors3);//'green','blue','yellow'
打印结果:
强大的数组操作方法splice()
splice()
方法主要有一下三种不同的使用方式:
下列示例展示了上述三种使用方式:
let colors = ['red','green','blue']; let removed = colors.splice(0,1); //删除第一项 console.log(colors); // green,blue console.log(removed); // red removed = colors.splice(1,0,'yellow','orange');//在位置1插入两个元素 console.log(colors);//green,yellow,orange,blue console.log(removed);//空数组 removed = colors.splice(1,1,'red','purple');//插入两个值,删除一个元素 console.log(colors);//green,red,purple,orange,blue console.log(removed);//yellow
JavaScript提供了两类搜索数组的方法:按严格相等搜索和断言函数搜索
严格相等
JS提供了三种严格相等的搜索方法:
这些方法都接收两个参数:要查找位置和一个可选的起始搜索位置。indexOf()
和includes()
方法从数组第一项开始向后搜索,而lastIndexOf()
从数组最后一项开始向前搜索。
indexOf()
和lastIndexOf()
方法都返回要查找的元素在数组中的位置,如果没找到则返回-1。includes()返回布尔值,表示是否至少找到一个与指定元素匹配的项。比较时会使用全等比较,即两项必须严格相等。
let num = [1,2,3,4,5,4,3,2,1]; console.log(num.indexOf(4),num.lastIndexOf(4),num.includes(4));//3,5,true console.log(num.indexOf(4,4),num.lastIndexOf(4,4),num.includes(4,7));//5,3,false let person = { name: "Nicholas" }; let people = [{ name: "Nicholas" }]; let morePeople = [person]; console.log(people.indexOf(person),morePeople.indexOf(person),people.includes(person),morePeople.includes(person));//-1,0,false,true
打印结果:
断言函数
断言函数接收三个函数:元素、索引和数组本身。其中元素是数组中当前搜索的元素,索引是当前元素的索引,而数组就是正在搜索的数组。断言函数返回真值,表示是否匹配。
find()
和findIndex()
方法使用了断言函数。这两个方法都从数组的最小索引开始。find()
返回第一个匹配的元素,findIndex()
返回第一个匹配元素的索引。这两个方法也都接收第二个可选的参数,用于指定断言函数内部this的值。另外,找到匹配项之后,这两个方法都不再继续搜索。
const people = [ { name:'matt', age:27 }, { name:'Nicholas', age:29 } ]; console.log(people.find((ele,index,arr) => ele.age < 28));//{name:'matt',age:27} console.log(people.findIndex((ele,index,arr) => ele.age < 28));//0
打印结果:
JavaScript中定义了5个迭代方法。每个方法接收两个参数:以每一项的参数运行的函数,以及可选的作为函数运行上下文的作用域对象(影响函数中this的值)。传给每个方法的函数接收3个参数:数组元素、元素索引和数组本身。数组的5个迭代方法如下:
some()和every()方法
let num = [1,2,3,4,5,4,3,2,1]; console.log(num.every((item,index,array) => item > 2)); //false console.log(num.some((item,index,array) => item > 2)); //true
打印结果:
filter()方法
这个方法基于给定的函数来决定某一项是否应该包含在它返回的数组中。
let num = [1,2,3,4,5,4,3,2,1]; console.log(num.filter((item,index,arr) => item > 2));//[3,4,5,4,3]
打印结果:
map()方法
这个数组的每一项都是对原始数组中同样位置的元素运行传入函数而返回的结果。
let num = [1,2,3,4,5,4,3,2,1]; console.log(num.map((item,index,arr) => item * 2));//[2,4,6,8,10,8,6,4,2]
打印结果:
forEach()方法
这个方法只会对每一项运行传入的函数,没有返回值。本质上,forEach()方法相当于使用for循环遍历数组。
let num = [1,2,3,4,5,4,3,2,1]; num.forEach(function(item,index,arr){ // 执行某些操作 });
JavaScript为数组提供了两个归并方法:reduce()
和reduceRight()
。这两个方法都会迭代数组的所有项,并在此基础上构建一个最终返回值。reduce()
方法从数组第一项开始遍历到最后一项。而reduceRight()
从最后一项开始遍历至最后一项。
这两个方法都接受两个参数:对每一项都会运行的归并函数,以及可选的以之为起点的初始值。传给reduce()
和reduceRight()
的函数接受四个参数:上一个归并值、当前项、当前项的索引和数组本身。这个函数返回的任何值都会作为下一次调用同一个函数的第一个参数。如果没有给这两个方法传入可选的第二个参数(作为归并的起点值),则第一次迭代将从数组的第二项开始,因此传给归并函数的第一个参数是数组的第一项,第二个参数是数组的第二项。
可以使用reduce()函数执行累加数组中多有数值的操作:
let values = [1,2,3,4,5]; let sum = values.reduce((prev,cur,index,arr) => prev + cur); console.log(sum);//15
第一次执行归并函数时,prev是1,cur是2。第二次执行时,prev是3(1+2),cur是3(数据第三项)。如此递进,直到把所有项都遍历一次,直到返回归并结果。
reduceRight()方法与之类似,只是方向相反:
let values = [1,2,3,4,5]; let sum = values.reduceRight((prev,cur,index,arr) => prev + cur); console.log(sum);//15
在这里,第一次调用归并函数时prev是5,而cur是4。