前端内参
搜索文档…
壹.2.9 强大的数组
在前端日常开发中,数组被使用得非常频繁。对数组各种常见方法充分掌握后,能有效提升工作效率。

壹.2.9.1 基本语法

数组是类似列表的高阶对象,JavaScript标准内置对象之一Array对象用于构造一个数组。
数组有三种创建方法:
1
//第一种 字面量
2
var arr0 = [element0, element1, ..., elementN]
3
//第二种 构造函数
4
var arr1 = new Array(element0, element1[, ...[, elementN]])
5
//第三种 构造函数
6
var arr2 = new Array(arrayLength)
Copied!

参数说明:

elementN
Array 构造器会根据给定的元素创建一个 JavaScript 数组,但是当仅有一个参数且为数字时除外(详见下面的 arrayLength 参数)。注意,后面这种情况仅适用于用 Array 构造器创建数组,而不适用于用方括号创建的数组字面量。
arrayLength
一个范围在 0 到 2^32-1 之间的整数,此时将返回一个 length 的值等于 arrayLength 的数组对象(言外之意就是该数组此时并没有包含任何实际的元素,不能理所当然地认为它包含 arrayLength 个值为 undefined 的元素)。如果传入的参数不是有效值,则会抛出 RangeError 异常。

壹.2.9.2 遍历数组

当面对一个数组的时候,我们经常需要对它进行遍历,从而让我们能够方便地对立面的每个元素进行操作。在开始正式内容之前,先来看看数组可以通过哪些方式进行遍历。
首先会想到 for 循环,通过声明一个变量作为下标能够方便地对所有元素进行操作。说到循环,那么其他的循环,比如 when 当然也是没问题的;通过下标的迭代,我们还可以使用递归来进行遍历。当我们着眼于 Array 本身时,我们会发现,在其原型链上为我们提供的forEach和map方法也能够对数组进行遍历。
那么下面,我们就来对上面说的几种方法中的三种:for、forEach、map进行一下剖析。
1
const arr = [0,1,2,3,4,5,6,7,8,9]
2
3
// for 循环
4
// 括号中第一个表达式 i 为迭代变量,第二个表达式为循环条件,第三个表达式更新迭代变量
5
for(let i = 0; i < arr.length; i ++) {...}
6
7
// forEach 遍历
8
// 必须传入一个回调函数作为第一个参数,该回调函数接受多个参数,第一个参数为当前数组遍历到的元素
9
arr.forEach(item => {...})
10
11
// map 遍历
12
// 必须传入一个回调函数作为第一个参数,该回调函数接受多个参数,第一个参数为当前数组遍历到的元素
13
arr.map(item => {...})
Copied!
从上面代码中可以发现,除了for循环以外,另外两种遍历似乎用法差不多,那是不是这两者可以通用,它们之间有没有差别呢?下面开始分析三种遍历方式的异同。

1. for

for 循环的遍历方式与另外两者的差别是最大的,通过代码块来执行循环。在代码块中,需要通过迭代变量来获取当前遍历的元素,如arr[i]
看上去通过迭代变量获取元素没有另外两种方式(能够直接获取)方便,但是在某些情况下,我们却不得不使用 for 循环:当在循环满足特定条件时跳出循环体,或跳出本次循环直接进行下一次循环。
1
let arr = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
2
for (let i = 0; i < arr.length; i++) {
3
// 当迭代变量 i == 3 时,跳过此次循环直接进入下一次
4
if (i == 3) continue;
5
console.log(arr[i]);
6
// 当 i > 7 时,跳出循环
7
if (i > 7) break;
8
}
9
10
//>> 0
11
//>> 1
12
//>> 2
13
//>> 4
14
//>> 5
15
//>> 6
16
//>> 7
17
//>> 8
Copied!
另外两种遍历方式,由于是通过回调函数的方式对遍历到的元素进行操作,即使在回调函数中 return ,也仅能够跳出当前的回调函数,无法阻止遍历本身的暂停。
1
let arr = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
2
arr.forEach(item => {
3
console.log(item);
4
if (item > 3) return; // 遍历并没有在大于 3 时结束
5
});
6
7
//>> 0
8
//>> 1
9
//>> 2
10
//>> 3
11
//>> 4
12
//>> 5
13
//>> 6
14
//>> 7
15
//>> 8
16
//>> 9
Copied!

2. forEach

forEach() 方法对数组的每个项执行一次提供的回调函数。
语法如下:
1
arr.forEach(callback[, thisArg]);
Copied!

参数说明:

callback 为数组中每个元素执行的函数,该函数接收三个参数:
  • currentValue数组中正在处理的当前元素。
  • index可选,数组中正在处理的当前元素的索引。
  • array可选,forEach() 方法正在操作的数组。
thisArg 可选参数。当执行回调函数时用作 this 的值(参考对象)。
由于匿名函数的 this 指向始终为 全局window 对象,然而某些情况下,我们需要改变 this 的指向,此时thisArg这个参数的作用就凸显出来了。
1
var a = "coffe";
2
var b = {a:"1891"};
3
(function() {
4
let arr = [0, 1, 2];
5
arr.forEach(function(item){
6
console.log(this.a);//这里是访问的b.a
7
},b);//这里把b作为thisArg参数传入之后,this就指向了b
8
})();
9
10
//>> 1891
11
//>> 1891
12
//>> 1891
Copied!
注 意: 如果使用箭头函数表达式来传入thisArg 参数会被忽略,因为箭头函数在词法上绑定了 this 值。
1
var a = "coffe";
2
var b = {a:"1891"};
3
(function() {
4
let arr = [0, 1, 2];
5
arr.forEach((item)=>{
6
console.log(this.a);//这里是访问的window.a
7
},b);//这里把b作为thisArg参数传入之后,本来this就应指向b,但由于使用了箭头函数表达式,
8
//this固定指向包含它的函数的外层作用域(也即匿名函数)的this,也即window
9
})();
10
11
//>> coffe
12
//>> coffe
13
//>> coffe
Copied!

3. map

map 的使用与 forEach 几乎一致,唯一的区别是:map 会返回一个新的数组,而这个数组的元素是回调函数的返回值,所以我们可以用一个变量接收 map 的返回值。
1
let arr = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
2
const arr1 = arr.map(item => item + 1);
3
console.log(arr1);
4
//>> [1,2,3,4,5,6,7,8,9,10]
5
6
const arr2 = arr.map(item => {item + 1});//注意这个回调箭头函数并没有返回值
7
console.log(arr2);
8
//输出一个数组项都为undefined的数组
9
//>> [undefined, undefined, …… undefined]
Copied!
上面的代码中,arr1将回调函数的返回值item + 1作为了数组中的元素,而arr2由于回调函数没有返回值,所以创建了一个每项都为undefined的数组。

4.for...in 与for...of

for...in遍历的是数组项的索引,而for...of遍历的是数组项的值。for...of遍历的只是数组内的项,而不包括数组的原型属性、方法,以及索引。
1
Array.prototype.getLength = function () {
2
return this.length;
3
}
4
var arr = [1, 2, 4, 5, 6, 7]
5
arr.name = "coffe1981";
6
console.log("-------for...of--------");
7
for (var value of arr) {
8
console.log(value);
9
}
10
console.log("-------for...in--------");
11
for (var key in arr) {
12
console.log(key);
13
}
14
15
//>> -------for...of--------
16
//>> 1
17
//>> 2
18
//>> 4
19
//>> 5
20
//>> 6
21
//>> 7
22
//>> -------for...in--------
23
//>> 0
24
//>> 1
25
//>> 2
26
//>> 3
27
//>> 4
28
//>> 5
29
//>> name
30
//>> getLength
Copied!
如上代码,会发现 for...in 可以遍历到原型上的属性和方法,如果不想遍历原型的属性和方法,则可以在循环内部用hasOwnPropery方法判断某属性是否是该对象的实例属性。
1
Array.prototype.getLength = function () {
2
return this.length;
3
}
4
var arr = [1, 2, 4, 5, 6, 7]
5
arr.name = "coffe1981";
6
console.log("-------for...in--------");
7
for (var key in arr) {
8
if(arr.hasOwnProperty(key))
9
console.log(key);
10
}
11
//>> -------for...in--------
12
//>> 0
13
//>> 1
14
//>> 2
15
//>> 3
16
//>> 4
17
//>> 5
18
//>> name
Copied!

总结:

for..of适用遍历数组/类数组对象/字符串/map/set等拥有迭代器对象的集合,但是不能遍历对象,因为没有迭代器对象。遍历对象通常用for...in来遍历对象的键名。
与forEach不同的是,for...of和for...in都可以正确响应break、continue和return语句。

壹.2.9.3 过滤方法 filter

filter() 方法返回一个新数组,其包含通过回调函数测试的所有数组项。
语法如下:
1
var newArray = arr.filter(callback(element[, index[, array]])[, thisArg])
Copied!
参数说明:
callback 用来测试数组的每个元素的函数。返回 true 表示该元素通过测试,保留该元素,false 则不保留。它接受以下三个参数:
  • element数组中当前正在处理的元素。
  • index可选,正在处理的元素在数组中的索引。
  • array可选,调用了 filter 的数组本身。
thisArg 可选参数。执行 callback 时,用于指定 this 的值。
1
const arr1 = [1, 4, 5, 6, 2, 3, 8, 9, 0];
2
3
const arr2 = arr1.filter((item, index, array) => {
4
return item > 5;
5
});
6
console.log(arr2);//>> [6, 8, 9]
Copied!
在上面的代码中,只要当前的数组项的值大于 5 ,item > 5就会返回true ,则会通过回调函数的测试,从而将该数组项保留,因此将原数组过滤后返回的新数组是[6, 8, 9]

壹.2.9.4 查找方法 find

find() 方法返回数组中通过回调函数测试的第一个数组项的值,如果没有通过测试则返回undefined
语法如下:
1
var item = arr.find(callback(element[, index[, array]])[, thisArg])
Copied!
参数说明与上面的filter一致,就不再赘述。示例代码如下:
1
let arr = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
2
const value = arr.find((item, index, array) => {
3
return item > 5
4
});
5
console.log(value); //>> 6
Copied!
需要注意的是,一旦回调函数测试通过(返回了 true) ,则 find 方法会立即返回当前数组项item的值;如果没有符合规则的数组项,则会返回undefined
与 find 类似的方法是 findIndex() 方法,区别在于 find 返回元素的值,而 findIndex则返回数组项的下标(索引)。

壹.2.9.5 some

some() 方法测试数组中是不是有数组项通过了回调函数的测试,返回一个Boolean类型的值。
语法如下:
1
arr.some(callback(element[, index[, array]])[, thisArg])
Copied!
参数说明与上面的filter一致。
注意:如果用一个空数组进行测试,在任何情况下它返回的都是false
示例代码如下:
1
let arr = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
2
const isTrue = arr.some((item, index, array) => {
3
return item > 5;
4
});
5
console.log(isTrue); //>> true
Copied!
与fitler和find相比,除了有返回值的区别,还有一个区别:如果原数组中存在被删除或者没有被赋值的索引,则回调函数在该数组项上不会被调用。是不是有点费解?看个代码示例就清楚了。
1
const arr = new Array(4);
2
arr[0] = 1;
3
const isTrue = arr.some((item) => {
4
return item > 5;
5
});
6
console.log(isTrue); //>> false
Copied!
在上面的例子中,虽然arr.length为 4 ,但是回调函数只在索引为 0 的项上被调用了,后面的三项由于未被赋值,所以不调用回调函数。

壹.2.9.6 sort

sort() 方法用原地算法对数组项进行排序,并返回数组。默认排序顺序是在将数组项转换为字符串,然后比较它们的UTF-16代码单元值。
原地算法(in-place algorithm)是一种使用小的、固定数量的额外空间来转换资料的算法,不随着算法执行而逐渐扩大额外空间的占用。当算法执行时,输入的资料通常会被要输出的部份覆盖掉。
由于排序取决于具体实现,因此无法保证排序的时间和空间复杂度。更多排序算法,可参考「贰.1.1 十大排序算法」。
语法如下:
1
var newArray = arr.sort([compareFunction]);
Copied!
参数说明:
compareFunction 可选,用来指定按某种顺序进行排列的函数。如果省略,元素按照转换为的字符串的各个字符的Unicode位点进行排序。
  • firstEl第一个用于比较的元素。
  • secondEl第二个用于比较的元素。
示例代码如下:
1
let arr = [1, 4, 5, 6, 2, 3, 8, 9, 0];
2
arr.sort((a, b) => {
3
return a - b;
4
});
5
console.log(arr);
6
//>> [0, 1, 2, 3, 4, 5, 6, 8, 9]
Copied!
sort 方法接收一个用于比较的回调函数,这个函数有两个参数,分别代表将要被比较的数组中的两个项,同时这两个数组项会按照回调函数的返回值进行排序:
  • 如果返回值小于 0 ,a 会被排在 b 之前;
  • 如果返回值大于 0 ,b 会被排在 a 之前;
  • 如果相等 , 则 a 和 b 的相对位置不变。
对于数字的升序排序,可以像下面这样写回调函数:
1
(a, b) => {
2
if (a < b) return -1;
3
if (a > b) return 1;
4
return 0;
5
}
Copied!
将上面的代码精简下,就会变成return a - b。 所以对于数字的排列,升序返回return a - b,降序返回return b - a
由于回调函数中的 a 和 b 分别是将要被比较的两个数组项,如果数组项是对象类型,也可以通过对象中的属性进行排序。
注意: sort 方法如果不传入比较的回调函数,那么它将会根据字符的 Unicode位点进行排序。
1
let arr = [2, 40, 11, 5, 10];
2
console.log(arr.sort());
3
//>> [10, 11, 2, 40, 5]
Copied!
上面的例子中, sort 没传入比较的回调函数,它会根据每个数组项的第一个字符进行排序,由于在 Unicode 中,1 在 2 之前,所以 10 会排在 2 之前,而不是根据数字 10 和 2 的大小来比较。如果两个数组项的第一个字符相同,则根据第二个字符对比排序。

壹.2.9.7 reduce

reduce() 方法对数组中的每个项执行一个由您提供的callback函数,将其结果汇总为单个值返回。

语法如下:

1
arr.reduce(callback(accumulator, currentValue[, index[, array]])[, initialValue])
Copied!

参数说明:

callback 执行数组中每个值的函数,包含四个参数:
  • accumulator **累计器,累计回调的返回值。它是上一次调用回调时返回的累积值,或initialValue(见于下方)。
  • currentValue 数组中正在处理的元素。
  • currentIndex 可选,数组中正在处理的当前元素的索引。 如果提供了initialValue,则起始索引号为0,否则为1。
  • array 可选调用reduce()的数组
initialValue 可选作为第一次调用 callback函数时的第一个参数的值。 如果没有提供初始值,则将使用数组中的第一个元素(也即针对数组的arr循环计算少一次,千万要注意这点)。 在没有初始值的空数组上调用 reduce 将报错。
reduce的定义比较抽象,平时开发中用的相对比较少,但若用好之后,能大大提升工作效率,所以这里我们重点介绍一下几种常见的用法示例。

1. 将一个数组类型转换成一个对象

我们可以使用reduce()来转换一个数组,使之成为一个对象。如果你想要做查询和分类,这个方法将非常有用。举一个例子,想象一下我们有以下peopleArr数组:
1
const arr = [
2
{
3
username: 'makai',
4
displayname: '馆长',
6
},
7
{
8
username: 'xiaoer',
9
displayname: '小二',
10
11
},
12
{
13
username: 'zhanggui',
14
displayname: '掌柜',
15
email: null
16
},
17
];
Copied!
在有些情况下,我们需要通过username来查询详细people详情,通常为了方便查询,我们需要将array转换成object。那么,通过使用reduce()方法,我们可以使用下面这种方法:
1
function callback(acc, person) {
2
//下面这句用到了扩展运算符...acc,表示把acc对象的属性“肢解”开,和新的属性一起
3
//以一个新的对象返回
4
return {...acc, [person.username]: person};
5
}
6
const obj = arr.reduce(callback, {});//这里的初始值为{}
7
console.log(obj);
8
//>> {
9
// "makai": {
10
// "username": "makai",
11
// "displayname": "馆长",
12
// "email": "[email protected]"
13
// },
14
// "xiaoer": {
15
// "username": "xiaoer",
16
// "displayname": "小二",
17
// "email": "[email protected]"
18
// },
19
// "zhanggui":{
20
// "username": "zhanggui",
21
// "displayname": "掌柜",
22
// "email": null
23
// }
24
// }
Copied!

2. 展开一个超大的array

通常我们会认为reduce()是用来精简一组数据的,来得到一个更简单的结果,这个简单结果当然也可以是一个数组。由于也从来没有明文规定说这个结果(数组)必须要比原来的的数组长度要小。所以,我们可以使用reduce()来把一个较短的数组转换成一个较长的数组。 当你需要从一个text文件里面去读取数据的时候,这种方法非常有用。下面是例子。假设我们已经读取到一系列简单文本数据,然后放入了一个数组。我们的需求是用逗号把它们分割,然后得到一个大的name 列表。
1
const arr = [
2
"Algar,Bardle,Mr. Barker,Barton",
3
"Baynes,Bradstreet,Sam Brown",
4
"Monsieur Dubugue,Birdy Edwards,Forbes,Forrester",
5
"Gregory,Tobias Gregson,Hill",
6
"Stanley Hopkins,Athelney Jones"
7
];
8
9
function callback(acc, line) {
10
return acc.concat(line.split(/,/g));
11
}
12
const arr1 = arr.reduce(callback, []);
13
console.log(arr1);
14
//>> [
15
// "Algar",
16
// "Bardle",
17
// "Mr. Barker",
18
// "Barton",
19
// "Baynes",
20
// "Bradstreet",
21
// "Sam Brown",
22
// "Monsieur Dubugue",
23
// "Birdy Edwards",
24
// "Forbes",
25
// "Forrester",
26
// "Gregory",
27
// "Tobias Gregson",
28
// "Hill",
29
// "Stanley Hopkins",
30
// "Athelney Jones"
31
// ]
Copied!
上面代码把一个length为5的数组,展开成了length为16的数组。

3. 完成对数组的两次计算,但只遍历一次

有时候我们需要对一个简单数组进行两次运算。比如计算出一组数字中的最大值和最小值。通常我们使用以下这种遍历两次的方法:
1
const arr = [0.3, 1.2, 3.4, 0.2, 3.2, 5.5, 0.4];
2
const maxReading = arr.reduce((x, y) => Math.max(x, y), Number.MIN_VALUE);
3
const minReading = arr.reduce((x, y) => Math.min(x, y), Number.MAX_VALUE);
4
console.log({minReading, maxReading});
5
//>> {minReading: 0.2, maxReading: 5.5}
Copied!
这种方法需要遍历两次数组。但是,现在有了一种不需要遍历次数这么多的方法。自从reduce()方法可以返回各种我们需要的类型。我们可以把两个值塞进同一个对象。这样我们就可以只遍历一次数组就可以做两次计算了。代码如下:
1
const arr = [0.3, 1.2, 3.4, 0.2, 3.2, 5.5, 0.4];
2
function callback(acc, reading) {
3
return {
4
minReading: Math.min(acc.minReading, reading),
5
maxReading: Math.max(acc.maxReading, reading),
6
};
7
}
8
const initMinMax = {
9
minReading: Number.MAX_VALUE,
10
maxReading: Number.MIN_VALUE
11
};
12
const result = arr.reduce(callback, initMinMax);
13
console.log(result);
14
//>> {minReading: 0.2, maxReading: 5.5}
Copied!

4. 在一次调用动作里,同时实现mapping和filter 的功能

假设我们有一个跟上文相同的peopleArr数组。我们现在要找出最近的登陆用户,并且去掉没有email地址的。一般情况下,我们通常使用下面这三个步骤的方法:
  1. 1.
    过滤掉所有没有email的对象;
  2. 2.
    提取最近的对象;
  3. 3.
    找出最大值。
放在一起我们可以得到如下代码:
1
function notEmptyEmail(x) {
2
return (x.email !== null) && (x.email !== undefined);
3
}
4
5
function getLastSeen(x) {
6
return x.lastSeen;
7
}
8
9
function greater(a, b) {
10
return (a > b) ? a : b;
11
}
12
13
const peopleWithEmail = peopleArr.filter(notEmptyEmail);
14
const lastSeenDates = peopleWithEmail.map(getLastSeen);
15
const mostRecent = lastSeenDates.reduce(greater, '');
16
17
console.log(mostRecent);
18
//>> 2019-05-13T11:07:22+00:00
Copied!
以上代码既兼顾了功能也拥有良好的可读性,同时对于简单的数据,可以运行良好。但是如果我们有一个巨大的数组,我们就有可能会碰上内存问题了。这是因为我们使用变量去储存了每一个中间数组。如果我们对reducer callback方法做一些改动,我们就可以一次性完成以上三步工作了。
1
function notEmptyEmail(x) {
2
return (x.email !== null) && (x.email !== undefined);
3
}
4
5
function greater(a, b) {
6
return (a > b) ? a : b;
7
}
8
function notEmptyMostRecent(currentRecent, person) {
9
return (notEmptyEmail(person))
10
? greater(currentRecent, person.lastSeen)
11
: currentRecent;
12
}
13
14
let result = peopleArr.reduce(notEmptyMostRecent, '');
15
16
console.log(result);
17
//>> 2019-05-13T11:07:22+00:00
Copied!
以上使用reduce()的代码仅仅只遍历了数组一次,极大地提升了性能。但是在数据量小的情况下,这种方法的性能优势不突出。

5. 运行异步方法队列

我们还可以做的一个操作是使用reduce(),可以在一个队列里面串联运行promise(相对于并行运行promise)。当需要请求一系列有速度限制的API,同时希望每个请求接连串来,上一个请求完成后才发出下一个请求的时候,下面这种方法就非常有用了。为了举一个例子,我们假设要从服务器取回peopleArr数组中的每一个people的消息。我们可以这样做:
1
function fetchMessages(username) {
2
return fetch(`https://example.com/api/messages/${username}`)
3
.then(response => response.json());
4
}
5
6
function getUsername(person) {
7
return person.username;
8
}
9
10
async function chainedFetchMessages(p, username) {
11
// 在这个函数体内, p 是一个promise对象,等待它执行完毕,
12
// 然后运行 fetchMessages().
13
const obj = await p;
14
const data = await fetchMessages(username);
15
return { ...obj, [username]: data};
16
}
17
18
const msgObj = peopleArr
19
.map(getUsername)
20
.reduce(chainedFetchMessages, Promise.resolve({}))
21
.then(console.log);
22
//>> {glestrade: [ … ], mholmes: [ … ], iadler: [ … ]}
Copied!
注意这段代码的逻辑,我们必须通过promise.resolve()调用promise回调函数,作为reducer的初始值。它会立刻调用resolve方法,这样一连串的API请求就开始接连运行了。
最近更新 4mo ago