js基础之探秘Array的原型方法

如果现在需要用js生成[0, 2, 4, 6, 8, …, 100]这样一个数组,你第一时间想到的会是下面的写法吗?

var arr = new Array(51);
//使用临时变量保存数组长度,可以有效避免每次循环都计算数组长度
var len = arr.length;  
for(var i = 0; i < len ; i++){
  arr[i] = i * 2;
}

或者你会想到这样写更好?

var arr = Array.apply(null, Array(51)).map((item, index) => {
  return index * 2;
})

再或者这样写?

var arr = new Array(51).fill(undefined).map((item, index) => {
  return index * 2;
})

很显然后面两种方式看起来更优雅。

不过有这样一组数据(仅用于比较速度快慢,实际数值与电脑性能有关):当上述数组长度为5万时,三者的用时分别为5毫秒、4毫秒、5毫秒(经过多次测试,第二种方式总是比其他两种略快一点),三者几乎不存在性能差距;而当这个长度增加到50万,三者的用时分别为:10毫秒、Max stack size(堆栈溢出)和21毫秒。当长度达到500万时,三者用时分别来到了34毫秒、Max stack size和148毫秒。

尽管后面两种方式所用到的数组原型方法都是用C++实现的,但是for循环的表现却比我们预期的要好得多(其实只要没有使用for in循环,差距都可以忽略不计)。

不过作为前端开发者,我们应该尽量避免使用for循环实现上述功能。当数组长度较小时,后面两种方式均可,当长度可能较大时最好还是使用第三种方式,避免因堆栈溢出而导致程序崩溃。原因是,一方面,for循环的语义不够明确,如果这是一个复杂的数组操作,往往会看得别人云里雾里。另一方面,使用原生的数组方法往往比我们自己实现更加安全可靠,它很少会产生一些怪异的行为(堆栈溢出不算是怪异行为,这里所说的怪异行为指程序看似正常执行,但结果却与预期不符)。

很多时候,如果你的程序中出现了上面这一小段优雅的代码,阅读代码的人马上就会对你刮目相看。但是话说回来,掌握数组的原生方法本就应是每个前端程序员的“第一课”,下面就让我们一起来探秘数组的静态方法(不带prototype的就是静态方法,它需要使用Array构造函数直接调用,比如下面的Array.from)及原型方法(你可以在数组实例上调用它们)。

1. Array.from( arrayLike[, mapFn[, thisArg]])

MDN描述 - 从一个类似数组或可迭代对象创建一个新的,浅拷贝的数组实例。

它最多接收三个参数,第一个是需要转化的类数组对象或可迭代对象;第二个是一个map函数,用于在转化后对新生成的数组的每个元素执行操作,可选;第三个是执行第二个map函数时的this对象,即引擎会在该对象上调用传入的第二个函数,可选。

比如:

function func(){
  var args = Array.from(arguments, function(item, index){
	return JSON.stringify(item);
  }, window);  //将arguments转化为数组,并将每个参数字符串化
}

函数内部的arguments就是一个类数组对象,它用于接收所有传入函数的参数,有length属性,且是可迭代的(iterable。也就是具有Iterator迭代器,该迭代器的next方法可以按序遍历出该对象的每一个属性)的。

但是它不是真正的数组,你无法在该对象上直接调用数组的原型方法。为了像真正的数组一样操作这个对象,我们使用Array.from将其转化为真正的数组([].slice.call(arguments)也是同样的作用,不过from方法看上去更加直接和优雅)。这样你就可以更简单地操作这个参数对象了。比如:

  ...
  var temp = args.slice(1); //提取从第二个到最后的所有参数

与之类似的对象还有DOM中的childNodes属性,严格来讲,它是NodeList类型的对象。但它具有length属性,并且可迭代,因此我们通常像处理数组一样来处理它。如果希望把它变成真正的数组,就可以使用上面的Array.from方法。而DOM在定义childNodes属性时之所以没有设置为数组,是因为NodeList具备随DOM结构变化而动态变化的能力,数组则不具备这种能力。

总之,像arguments和childNodes这样具有length属性,并且内部实现了迭代器Iterator的对象,都可以使用Array.from来转化为真正的数组。不过注意,这个转化只是个浅拷贝,也就是说,如果原对象的某个属性是对象,那么创建出来的数组和原属性引用的是同一个对象,因此对这类属性的操作会对两者同时造成影响。为了避免这种影响,你可以简单实用下面的语法来进行一次深拷贝:

JSON.parse(JSON.stringify(arr));

把得到的数组经过上述转化,就可以创建一个与原对象无关的新数组,从而实现了深拷贝。

2. Array.isArray(obj)

MDN描述 - 用于确定传递的值是否是一个 Array。

用于判断当前对象是不是数组,使用方法很简单,将需要判断的对象传入该方法,如果它是数组,将返回true,否则将返回false。

Array.isArray的检测原理类似于instanceof。但是在涉及iframes时,它的优先级应高于instanceof,因为instanceof不能跨iframe检测数组,比如下面的例子:

//在页面中创建一个iframe
var iframe = document.createElement('iframe');
document.body.appendChild(iframe);
//拿到iframe内的Array构造函数的引用,
//它与当前环境的Array并不是同一个构造函数
xArray = window.frames[window.frames.length-1].Array;

//使用iframe内的Array构造函数构造了一个数组
var arr = xArray(1,2,3);  //[1,2,3]

arr instanceof Array;//false,iframe内的Array与当前环境
					 //的Array不是同一个,检测失败
Array.isArray(arr); //true,isArray成功检测到它是一个数组

涉及跨iframe检测数组时要注意这一点。

3. Array.of(element0[, element1[, …[, elementN]]])

MDN描述 - 创建一个具有可变数量参数的新数组实例,而不考虑参数的数量或类型。

用过js的同学应该知道,js的数组存在一个似乎不太合常理的行为。如果你在构造函数中传入超过一个参数,那么他们将成为数组的元素:

var arr = new Array(10, 20);  //[10, 20];

但是如果只传入一个整数,引擎却会为你创建一个该长度的空数组:

var arr = new Array(10); //[],长度为10,每个元素都是empty

如果你希望用这种方式创建仅有一个元素为10的数组,那么你得到的结果可能不会符合预期。但由于历史原因,纠正这种行为也不太可能。

因此es规范推出了Array.of()来提供更规范的实现。传入Array.of()的参数都将作为数组元素创建新数组。现在你可以像下面这样创建[10]:

var arr = Array.of(10);

它的职责看起来更单一,因此你不需要担心出现什么怪异行为。

4. Array.prototype.concat()

MDN描述 - 用于合并两个或多个数组。此方法不会更改现有数组,而是返回一个新数组。

语法:var new_array = old_array.concat(value1[, value2[, …[, valueN]]])

它会将多个数组的元素拷贝出来,构成一个数组。如下:

var arr1 = [1,2];
var arr2 = [3];

var arr3 = arr1.concat(arr2); //[1,2,3]

concat不会影响原数组,而是返回一个新数组。并且所执行的是浅拷贝,也就是说,如果数组的某个元素是对象,那么返回的新数组将持有与原数组相同的引用,修改其中一个的值,都会立即影响到另一个数组中该元素的值。

如果传入concat的某个参数不是数组,那么它将作为单个元素被添加到最终返回的数组,如:

传入的对象{carter:24}被视为了[{carter:24}]进行拼接
var arr = [1,2].concat([3], {carter: 24}); //[1,2,3,{carter: 24}]

5. Array.prototype.copyWithin()

MDN描述 - 浅复制数组的一部分到同一数组中的另一个位置,并返回它,不会改变原数组的长度。

语法:arr.copyWithin(target[, start[, end]])

它首先提取从start到end-1位置的数组元素(类似于slice的行为),然后从target位置开始进行替换,最后返回修改后的数组。比如我希望把数组的最后两个元素复制并替换到数组的开头位置,那么target就是0(因为需要替换开头的元素),start是arr.length - 2,表示从索引为arr.length - 2的位置开始复制,end为arr.length,表示复制的结束位置为arr.length - 1,它是数组的最后一个元素。

var arr = [1,2,3,4,5];
arr.copyWithin(0, arr.length - 2, arr.length);

arr //=>[4, 5, 3, 4, 5];

这里的start和end为可选参数,start默认为0,end默认为arr.length。

为什么end表示复制到数组的end - 1的位置呢?这是因为数组的下标是从0开始的,也就是说数组的第一个元素是arr[0]。如果数组的长度为10,那么它的最后一个元素是arr[9]。所以如果end表示的是结束位置的索引值,那么你每次想复制到最后一个元素,都必须传入end的值为arr.length - 1,这是很令人讨厌的行为。于是工作组规定,复制到end的前一个位置就会结束,现在你可以优雅地将arr.length作为end传入函数。类似的还有slice这样的函数。

6. Array.prototype.entries()

MDN描述 - 返回一个新的Array Iterator对象,该对象包含数组中每个索引的键/值对。

语法:arr.entries()

该原型方法返回一个迭代器对象,通过该对象,你可以按顺序迭代数组的每个元素。比如:

var arr = ["a", "b", "c"];
var iterator = arr.entries();
console.log(iterator);

/*Array Iterator {}
         __proto__:Array Iterator
         next:ƒ next()
         Symbol(Symbol.toStringTag):"Array Iterator"
         __proto__:Object
*/

对于数组arr,调用entries方法得到了一个Iterator对象(也称为遍历器),它最主要的作用是向外暴露了next方法,如果尝试调用next方法,你会得到下面一个对象:

iterator.next() //{value: [0, "a"], done: false}

value是由键值对构成的数组,由于当前被遍历的是一个数组,因此这里的键就是元素的索引,而值就是元素的值。done表示当前遍历是否结束,为true则表示遍历结束。因此你可以像下面一样遍历数组:

for(var i = 0; i < arr.length + 1; i++){
  var tem = iter.next(); // 每次迭代时更新next
  console.log(tem);
  
  if(!tem.done){
    ...   //对该元素执行一些操作
  }
}

可以看到在控制台将输出这样一组对象:

{value: [0, "a"], done: false}
{value: [1, "b"], done: false}
{value: [2, "c"], done: false}
{value: undefined, done: true}

之所以for循环的结束条件需要写为i < arr.length + 1,是因为Iterator在输出最后一个元素后并不知道遍历已经结束了,只有多检查一次才知道所有的属性都输出完毕了。

entries方法使得遍历数组元素变得非常灵活和安全。假如你对数组元素的操作无法在一个for循环内完成(如存在异步操作或跨模块问题),那么你就必须借助一个变量来记录当前你处理到了第几个元素,这样使得你的遍历操作内聚性非常差,并因此会导致一个安全问题 – 你无法保证你用于记录索引的这个变量不会被无意或恶意地修改。而通过entries方法得到遍历器,你可以在任何位置调用next方法获取下一个要操作的元素,遍历器自身可以记录当前处理到第几个元素。

Iterator遍历器是es6中非常重要的一个对象,如果你希望自定义的js对象可以使用for … of语句进行遍历,那么你只需要手动为该对象实现一个遍历器属性即可。此外,实现了遍历器的对象可以使用上面讲到的Array.from转化为数组,一些复杂的处理将变得非常方便。遍历器不是本文的重点,感兴趣的可以自行查阅资料了解。

7. Array.prototype.every()

MDN描述 - 测试一个数组内的所有元素是否都能通过某个指定函数的测试。它返回一个布尔值。

语法:arr.every(callback[, thisArg])

这是一个较为常见的函数,用于检查当前数组的每个元素是否都能满足某个条件。如果将元素传入callback,返回了true,则表示通过测试,否则表示未通过测试。如果数组的每个元素都通过测试,则every方法返回true,否则返回false。如:

[1,2,3,4,5,6,7].every(function(item, index, array){
  return item < 10;
})   //true

[1,2,3,14,5,6,57].every(function(item, index, array){
  return item < 10;
})   //false

上述回调函数用于检查是否每个数组元素都小于10,第一个数组通过了测试,而第二个没有通过。

回调函数最多接收三个参数,分别是当前的数组元素(上述item)、该元素的索引值(index)和数组本身(array)。由于这里的参数只是形参,因此它们的名字可以随意起,其实际值由引擎执行该函数时传入。

8. Array.prototype.fill()

MDN描述 - 用一个固定值填充一个数组中从起始索引到终止索引内的全部元素。不包括终止索引。

语法:arr.fill(value[, start[, end]])

从字面意思理解,就是使用传入的value值填充数组。填充区间为start ~ end - 1。这里end的含义与copyWithin类似,它表示填充到索引为end的前一个位置,因此你可以直接传入arr.length指代填充到最后一个元素。

注意,使用new Array()创建出的空数组与使用fill(null)或fill(undefined)填充数组得到的结果是不一样的。如:

var a = new Array(10);//[empty * 10]
console.log(a[0]); //undefined

a.fill(null); [null, null, ... null];
console.log(a[0]); //null
a.fill(undefined); [undefined, undefined, ... undefined]
console.log(a[0]); //undefined

填充为null的数组自然与不填充是不一样的,因为null本身是个对象,有自己的内存地址。但是填充为undefined和不进行填充有差别吗?

当然是有的。还记得我们最开始的例子吗?我们使用:

var arr = new Array(51).fill(undefined).map((item, index) => {
  return index * 2;
})  //[0, 2, 4, ..., 100]

而不是:

var arr = new Array(51).map((item, index) => {
  return index * 2;
})   //[empty × 51]

使用fill填充最初的数组后,我们得到了预期的结果。但是如果不使用fill进行填充,我们得到的仍然是长度为51的空数组。这是因为对于空元素,js引擎不会为其执行map回调函数,但是对于值为undefined的元素,js引擎会执行该回调函数。当某个元素为empty时,js引擎会告诉你它的值为undefined,但这只是js引擎的默认行为,你必须清楚的是,数组元素为empty和undefined对引擎来说并不完全等价。

9. Array.prototype.filter()

MDN描述 - 创建一个新数组, 其包含通过所提供函数实现的测试的所有元素。

语法:var newArray = arr.filter(callback(element[, index[, array]])[, thisArg])

同样非常常见的一个方法,从字面意思理解,就是过滤数组。它只会留下能够通过回调函数测试的那些元素。如:

//如果用不到index和array,这里也可以不写
[1,72,3,4,15,6,7,38,9].filter((item, index, array) => {
  return item > index * 2;
})   => [72, 15, 38]

上述代码返回比自身索引值两倍还大的数组元素,最终得到了由72,15和38构成的一个数组。注意,该方法不会修改原数组,而是创建一个新数组。

一个经常犯错的点是,即使数组中必定只有一个元素满足条件,得到的结果也是一个数组,因此获取该元素时请取结果数组的第一个元素。

10. Array.prototype.find()

MDN描述 - 返回数组中满足提供的测试函数的第一个元素的值。否则返回 undefined。

语法:arr.find(callback[, thisArg])

类似于filter的作用,但它只返回符合条件的第一个数组元素,得到的结果是符合条件的第一个元素,而不是一个数组。

如果你清楚知道数组中必定只有一个满足条件的元素,或者你只关心第一个满足条件的元素,那么你可以使用find方法。它的用法与filter类似:

[1,72,3,4,15,6,7,38,9].find((item, index, array) => {
  return item > index * 2;
})   => 72

11. Array.prototype.findIndex()

MDN描述 - 返回数组中满足提供的测试函数的第一个元素的索引。否则返回-1。

语法: arr.findIndex(callback[, thisArg])

这是find的姊妹方法,用法几乎完全一致,只是它返回的是第一个符合条件的元素的索引值。这里不再详述。

12. Array.prototype.flat()

MDN描述 - 按照一个可指定的深度递归遍历数组,并将所有元素与遍历到的子数组中的元素合并为一个新数组返回。

语法:var newArray = arr.flat([depth])

这是数组的扁平化方法,用于数组降维。接收的参数为需要降低的维数。如:

[[1,2], [3], 4].flat(1); //[1,2,3,4]

[[[1],2], [3], 4].flat(1); //[[1],2,3,4]

[[[2],2], [3], 4].flat(2); //[1,2,3,4]

传入的depth为1时,可以把二维数组降为一维数组,或把三维数组降为二维数组等。传入的depth为2时,可以把三维数组降为一维数组,以此类推。不传depth时,默认值为1。如果当前数组的维数小于depth,那么降到一维后就直接返回。

如果你希望无论传入多少维的数组,都压缩成一维数组,那么你可以将depth设置为Infinity(即flat(Infinity)),它在js中表示无穷大,是一个常量。

值得注意的是,flat方法会去除数组中的空元素。比如:

[1,2,3, ,5].flat() //[1,2,3,5]

var arr = new Array(10).flat() //[]

13. Array.prototype.flatMap()

MDN描述 - 首先使用映射函数映射每个元素,然后将结果压缩成一个新数组。

语法:var new_array = arr.flatMap(function callback(currentValue[, index[,array]]) {
// 返回新数组的元素
}[, thisArg])

这个函数就是map和flat方法的组合,只是flat的depth只能为1。关于map方法后面会介绍到。它进行的操作就是,首先调用传入的函数(也就是用于映射的map函数)得到结果数组,然后对这个数组执行flat(1)进行降维。

下面的两种写法几乎完全等价:

[1,2].flatMap((item, index) => {
  return [index, item];
})   //[0, 1, 1, 2]

[1,2].map((item, index) => {
  return [index, item];
}).flat(1)   //[0, 1, 1, 2]

14. Array.prototype.forEach()

MDN描述 - 对数组的每个元素执行一次提供的函数。

语法:arr.forEach(callback[, thisArg]);

该方法没有返回值,也不会修改原数组,它的行为只是对数组的每个元素执行一次传入的回调函数。比如你希望按序输出数组的每个元素,使用forEach方法可以这么写:

[1,2,3,4,5].forEach(item => {
  console.log(item)
})  //依次输出1 2 3 4 5

15. Array.prototype.includes()

MDN描述 - 用来判断一个数组是否包含一个指定的值,根据情况,如果包含则返回 true,否则返回false。

语法: arr.includes(valueToFind[, fromIndex])

第一个参数为需要检查的数组元素,第二个为查找起点,表示从某个元素之后开始查找,该参数可选。如果找到了该元素,则返回true,否则返回false。

注意,该方法的比对规则与===类似,是地址比对,这也就意味着你无法在数组中查找对象元素:

[{}].includes({});  //false,因为{} !== {},两者在内存中有不同的地址

该方法的用途也很简单,就是检查某个数组中是否存在某个元素。

16. Array.prototype.indexOf()

MDN描述 - 返回在数组中可以找到一个给定元素的第一个索引,如果不存在,则返回-1。

语法:arr.indexOf(searchElement[, fromIndex])

这可能是最常用的数组原型方法之一了。它最多接收两个参数,需要查找的元素和查找起点,返回该元素在数组中的索引值。如果没有查找到该元素,则返回-1。因此我们经常看到下面的代码来检测某个元素是否存在于数组中:

if(arr.indexOf(target) > -1){
  ...
}

如果你只是希望检测target是否存在与arr中,那么你应该使用arr.includes(target),而不是indexOf,因为在这里,前者的语义更为明确。

17. Array.prototype.join()

MDN描述 - 将一个数组(或一个类数组对象)的所有元素连接成一个字符串并返回这个字符串。如果数组只有一个项目,那么将返回该项目而不使用分隔符。

语法:arr.join([separator])

也是一个常用的数组原型方法。它使用传入的字符来将数组连接为字符串,这种操作常见于在页面上显示数组。join的默认参数是英文中的逗号“,”,如:

["小明","小红"].join();  //“小明,小红”

["小明","小红"].join("-");  //“小明-小红”

在中文环境下,我们通常传入中文的逗号“,”作为分隔符。与英文的逗号不同的是,中文的逗号与一个汉字是等宽的,而英文逗号只有半个汉字的宽度。

数组的join方法与字符串的split方法是一对互操作,比如上面的字符串使用split方法可以再转化成数组:

"小明-小红".split("-"); //["小明","小红"]

这组互操作在数据显示中非常常用。

18. Array.prototype.keys()

MDN描述 - 返回一个包含数组中每个索引键的Array Iterator对象。

语法:arr.keys()

该方法与entries方法类似,但是它返回的迭代器只能迭代出数组每个元素的索引值,相对于Object原型上的keys方法,该方法的价值似乎比较小,因为数组元素的索引值不太需要专门的遍历器来遍历,我们更关心的是数组的元素。我们知道,js中数组就是对象,那何不用对象的keys方法来遍历数组呢?这是因为对象的keys方法无法遍历出空元素的键,比如:

var arr = ["a", , "c"];

var sparseKeys = Object.keys(arr);
var denseKeys = [...arr.keys()];

console.log(sparseKeys); // ['0', '2']
console.log(denseKeys);  // [0, 1, 2]

该方法更多是为了数组对象的完整性而设计的,比较少用,这里不再详述。

19. Array.prototype.lastIndexOf()

MDN描述 - 返回指定元素(也即有效的 JavaScript 值或变量)在数组中的最后一个的索引,如果不存在则返回 -1。从数组的后面向前查找,从 fromIndex 处开始。

语法:arr.lastIndexOf(searchElement[, fromIndex])

indexOf的孪生方法,与indexOf不同的是,该方法是从后往前查找,找到第一个匹配的元素之后就返回它的索引值。如果传入了fromIndex,将从该位置向前查找。

20. Array.prototype.map()

MDN描述 - 创建一个新数组,其结果是该数组中的每个元素都调用一个提供的函数后返回的结果。

语法:var new_array = arr.map(function callback(currentValue[, index[, array]]) {
// Return element for new_array
}[, thisArg])

如果你经常使用js,那么这个原型方法可能是你最常用的原型方法之一了。比如我们最开始生成那个数组的后面两个方法的优雅之处就在于用到了map函数。它的作用是映射数组元素。比如我们有下面一个数组:

var arr = [1,2,3]

我们希望把它映射为:[2,4,6],于是你可以这样写:

var arr2 = arr.map((item, index) => {
  return item * 2;
})

注意,map函数不会改变原数组,因此你必须使用一个变量接收操作结果。如果原数组已经没用了,你也可以赋值到原数组。

上面代码的含义是,将原数组映射到另一个结果数组,由传入map的函数定义映射规则。这里所给的映射规则为:结果数组的元素是原数组元素的2倍。注意,映射关系是建立在两个数组相同索引的元素上的。也就是:

(item => item * 2)
arr[0] = 1   =>  arr[2] = 2
arr[1] = 2   =>  arr[1] = 4
arr[2] = 3   =>  arr[2] = 6

arr => [1,2,3]
arr2 => [2,4,6]

js引擎每次会按序把一个原数组元素传入回调函数,得到结果数组中对应位置的元素。当原数组元素遍历完,映射过程就结束了。

21. Array.prototype.push/pop/unshift/shift()

这是四个数组的原型方法,分别是从尾部添加和删除一个元素,从头部删除和添加一个元素。

如果学过C语言的话应该知道,C语言中存在栈和队列的概念。栈只允许从顶部(也就是尾部)增删元素,而队列只允许在尾部增加元素,在头部删除元素(就像排队一样,先入先出)。js中没有直接定义栈和队列这样的数据结构,而是使用数组来模拟两者的行为。

push和pop用于模拟栈的行为,push为从数组尾部插入元素,pop为从数组尾部删除一个元素。前者没有返回值,但会改变原数组,后者也会改变原数组,并且返回被删除的元素。

shift和push可以模拟队列的行为,push与栈的行为一致,而shift会从数组头部删除一个元素(出队操作),并返回这个元素。为了方便,js还扩充了unshift方法,它是shift的逆操作,即从数组头部插入一个元素。

有了这四个方法,你可以像操作C语言的栈和队列一样操作js的数组。

22. Array.prototype.reduce()

reduce: 减少,缩小。

MDN描述 - 对数组中的每个元素执行一个由您提供的reducer函数(升序执行),将其结果汇总为单个返回值。

语法:arr.reduce(callback(accumulator, currentValue[, index[, array]])[, initialValue])

reduce本身可以接受两个参数:reducer函数和初始值initialValue,前者是定义如果汇总数组元素,后者定义初始值,如果不传初始值,数组的第一个元素将作为初始值使用,并且reduce将跳过第一个元素,从第二个元素开始执行reducer函数。

reducer 函数接收4个参数:

  1. Accumulator (acc) (累计器)
  2. Current Value (cur) (当前值)
  3. Current Index (idx) (当前索引)
  4. Source Array (src) (源数组)

reducer 函数的返回值会被分配给累计器,该返回值在数组的每个迭代中被记住,并最后成为最终的单个结果值。

这是一个强大但经常被忽略的数组原型方法,开发者们倾向于使用复杂的for循环和一些变量来实现reduce本可以实现的功能。reduce的作用是依次处理数组中的每一项,把每次的处理结果传递给下次继续处理,直到数组处理完毕,得到一个最终结果(也就是汇总)。一个非常典型的用法是数组求和:

var arr = [1,2,3,4,5,6,7];
arr.reduce((total, currentVal, index, src) => {
  return total + currentVal;
})    //输出28,即原数组每项的和

上面的代码中,由于没有传入初始值,js引擎以第一项1作为初始值,然后从第二项2开始进行求和。对2求和时,把之前的求和结果1作为total参数传入,得到第一步的汇总值3。然后引擎继续对第三个元素进行汇总,并把之前的汇总结果3作为total参数传入,继续求和,得到汇总值6。以此类推,直到汇总到数组的最后一个元素7,返回最终的汇总结果28。

如果传入了初始值,那么js引擎将从第一个元素开始汇总,并以初始值作为累加结果传入回调函数。

这里取名reduce的本意,就是将一个数组从某个长度缩减到单个的汇总结果。React的核心库redux中有一个reducer的概念,也正是借鉴的这种思想。

23. Array.prototype.reduceRight()

上述方法的孪生方法,用于从后向前汇总数组元素,所传参数与上述方法一致,这里不再详述。

24. Array.prototype.reverse()

MDN描述 - 将数组中元素的位置颠倒,并返回该数组。数组的第一个元素会变成最后一个,数组的最后一个元素变成第一个。该方法会改变原数组。

语法: arr.reverse()

用于颠倒数组元素,比如:

[1,2,3,4].reverse()  => [4,3,2,1]

它不接受任何参数,返回颠倒后的结果。该方法会改变原数组,因此你不必使用变量来接收操作结果。

25. Array.prototype.slice()

MDN描述 - 返回一个新的数组对象,这一对象是一个由 begin 和 end 决定的原数组的浅拷贝(包括 begin,不包括end)。原始数组不会被改变。

语法:arr.slice([begin[, end]])

数组的切片方法。你可以使用该方法从数组中提取若干元素,且不会影响到原数组。它接收两个参数,start和end,表示从start位置开始,一直取到end - 1的位置,end默认为数组长度,即截取到最后一个元素。比如:

var a = [1,2,3,4,5];

var b = a.slice(2);  //[3,4,5]
var c = a.slice(2, 4);  //[3,4]
a   //[1,2,3,4]

除了常见的数组操作,slice还长被用于将类数组对象转化为数组,即前文提到的Array.slice.call(arguments)。

26. Array.prototype.some()

MDN描述 - 测试数组中是不是至少有1个元素通过了被提供的函数测试。它返回的是一个Boolean类型的值。

语法:arr.some(callback(element[, index[, array]])[, thisArg])

该方法与every对应,every用于检测是否所有的元素都能通过回调函数测试,some用于检测是否存在可以通过测试的数组元素。这里只要有一个元素可以使回调函数返回true,则some方法立即返回true,如果全部测试失败,则some方法返回false。

具体用法请参考every方法。

27. Array.prototype.sort()

MDN描述 - 用原地算法对数组的元素进行排序,并返回数组。默认排序顺序是在将元素转换为字符串,然后比较它们的UTF-16代码单元值序列时构建的。

语法:arr.sort([compareFunction])

它可以对数组元素进行排序,接收一个排序函数作为参数。js引擎每次将两个数组元素作为参数传入排序函数,根据返回的数值的正负判断哪个元素排在前面。如果返回负数,则传入的前一个元素应该排在前面,否则后一个应该排在前面。如:

[1,8,4,6,2,9].sort((num1, num2) => {
  return num1 - num2;
})   //[1,2,4,6,8,9]

[1,8,4,6,2,9].sort((num1, num2) => {
  return num2 - num1;
})   //[9,8,6,4,2,1]

sort方法的排序规则相当灵活,如果感兴趣可以参考之前的JavaScript的函数式特性一文。

28. Array.prototype.splice()

MDN描述 - 通过删除或替换现有元素或者原地添加新的元素来修改数组,并以数组形式返回被修改的内容。此方法会改变原数组。

语法:array.splice(start[, deleteCount[, item1[, item2[, …]]]])

增删数组元素的一个非常灵活的方法。它可以在任意位置删除若干个元素,或者插入若干个元素。它接收的第一个参数是进行操作的索引位置,js引擎将从该位置删除或新增元素。第二个参数是要删除的元素数量,如果你不需要删除元素,这个值传入0即可。后面的参数都是需要插入到该位置的元素。注意,如果你没有执行删除就想该位置插入元素,那么该位置原来的元素将被排在所插入的所有元素的后面。如:

 //[1,5,6],从index为1的位置删除三个元素
[1,2,3,4,5,6].splice(1, 3);

//[1,7,8,2,3,4,5,6],从index为1的位置插入7和8两个元素
[1,2,3,4,5,6].splice(1, 0, 7, 8); 

//[1,9,3,4,5,6]删除index为1的元素,并添加元素9,也就是替换该元素
[1,2,3,4,5,6].splice(1, 1, 9);

29. Array.prototype.toLocaleString/toString()

MDN描述 - 返回一个字符串表示数组中的元素。

语法:arr.toLocaleString([locales[,options]]);

将数组转化为字符串,并且基于特定语言环境。比如当前设置语言环境为中文,那么拼接字符串时将使用中文的逗号隔开,如果某个元素是日期,也会使用中文格式来描述。与此对应的是Array.prototype.toString(),它以国际标准语言环境(英文)将数组转化为字符串,此时数组元素将以英文的逗号隔开,日期类型的元素也是英文格式的。

关于语言环境代码可以自行查阅资料,比较常见的语言环境如英文为:“en”,中文为“zh”等。

30. Array.prototype.values()

MDN描述 - 返回一个新的 Array Iterator 对象,该对象包含数组每个索引的值。

语法:arr.values()

该方法与entries和keys是类似的。只是entries返回的遍历器用于遍历键值对,keys返回的用于遍历键,那么values返回的遍历器就是用于遍历元素值。相对于keys方法,values方法在数组遍历中价值更高。比如你可以像下面一样遍历数组元素:

const array1 = ['a', 'b', 'c'];
const iterator = array1.values();

for (const value of iterator) {
  console.log(value); // 依次输出"a" "b" "c"
}

同样,你可以使用next方法依次遍历出数组的每一个元素。它也包含一个done属性,用来标记当前遍历是否结束。

总结

这里汇总了目前正在使用的几乎所有的数组原型方法,一些不推荐或者已经废弃的方法,如Array.observe、Array.toSource并没有介绍,我们也不必关心这些方法的用法。本文篇幅有限,只能介绍这些方法的简单用法,如果感兴趣,请参阅MDN开发者文章进行深入学习Array.prototype。熟练运用这些原型方法对学习js至关重要,需要日常工作中大量积累才能掌握。

你可能感兴趣的:(js基础,JS基础,数组,原型方法)