强大的数组

本文阅读 20 分钟
首页 知识库 正文

在前端日常开发中,数组被使用得非常频繁。对数组各种常见方法充分掌握后,能有效提升工作效率。

基本语法

数组是类似列表的高阶对象,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)

参数说明:

elementN

Array 构造器会根据给定的元素创建一个 JavaScript 数组,但是当仅有一个参数且为数字时除外(详见下面的 arrayLength 参数)。注意,后面这种情况仅适用于用 Array 构造器创建数组,而不适用于用方括号创建的数组字面量。

arrayLength

一个范围在 0 到 2^32-1 之间的整数,此时将返回一个 length 的值等于 arrayLength 的数组对象(言外之意就是该数组此时并没有包含任何实际的元素,不能理所当然地认为它包含 arrayLength 个值为 undefined 的元素)。如果传入的参数不是有效值,则会抛出 RangeError 异常。

遍历数组

当面对一个数组的时候,我们经常需要对它进行遍历,从而让我们能够方便地对立面的每个元素进行操作。在开始正式内容之前,先来看看数组可以通过哪些方式进行遍历。

首先会想到 for 循环,通过声明一个变量作为下标能够方便地对所有元素进行操作。说到循环,那么其他的循环,比如 when 当然也是没问题的;通过下标的迭代,我们还可以使用递归来进行遍历。当我们着眼于 Array 本身时,我们会发现,在其原型链上为我们提供的forEach和map方法也能够对数组进行遍历。

那么下面,我们就来对上面说的几种方法中的三种:for、forEach、map进行一下剖析。

  1. const arr = [0,1,2,3,4,5,6,7,8,9]
  2. // for 循环
  3. // 括号中第一个表达式 i 为迭代变量,第二个表达式为循环条件,第三个表达式更新迭代变量
  4. for(let i = 0; i < arr.length; i ++) {...}
  5. // forEach 遍历
  6. // 必须传入一个回调函数作为第一个参数,该回调函数接受多个参数,第一个参数为当前数组遍历到的元素
  7. arr.forEach(item => {...})
  8. // map 遍历
  9. // 必须传入一个回调函数作为第一个参数,该回调函数接受多个参数,第一个参数为当前数组遍历到的元素
  10. arr.map(item => {...})

从上面代码中可以发现,除了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. //>> 0
  10. //>> 1
  11. //>> 2
  12. //>> 4
  13. //>> 5
  14. //>> 6
  15. //>> 7
  16. //>> 8

另外两种遍历方式,由于是通过回调函数的方式对遍历到的元素进行操作,即使在回调函数中 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. //>> 0
  7. //>> 1
  8. //>> 2
  9. //>> 3
  10. //>> 4
  11. //>> 5
  12. //>> 6
  13. //>> 7
  14. //>> 8
  15. //>> 9

2. forEach

forEach() 方法对数组的每个项执行一次提供的回调函数。

语法如下:

  1. arr.forEach(callback[, thisArg]);

参数说明:

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. //>> 1891
  10. //>> 1891
  11. //>> 1891
注 意:
如果使用箭头函数表达式来传入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. //>> coffe
  11. //>> coffe
  12. //>> coffe

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. const arr2 = arr.map(item => {item + 1});//注意这个回调箭头函数并没有返回值
  6. console.log(arr2);
  7. //输出一个数组项都为undefined的数组
  8. //>> [undefined, undefined, …… undefined]

上面的代码中,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. //>> -------for...of--------
  15. //>> 1
  16. //>> 2
  17. //>> 4
  18. //>> 5
  19. //>> 6
  20. //>> 7
  21. //>> -------for...in--------
  22. //>> 0
  23. //>> 1
  24. //>> 2
  25. //>> 3
  26. //>> 4
  27. //>> 5
  28. //>> name
  29. //>> getLength

如上代码,会发现 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

总结:

for..of适用遍历数组/类数组对象/字符串/map/set等拥有迭代器对象的集合,但是不能遍历对象,因为没有迭代器对象。遍历对象通常用for...in来遍历对象的键名。

与forEach不同的是,for...of和for...in都可以正确响应break、continue和return语句。

过滤方法 filter

filter() 方法返回一个新数组,其包含通过回调函数测试的所有数组项。

语法如下:

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

参数说明:

callback 用来测试数组的每个元素的函数。返回 true 表示该元素通过测试,保留该元素,false 则不保留。它接受以下三个参数:

  • element数组中当前正在处理的元素。
  • index可选,正在处理的元素在数组中的索引。
  • array可选,调用了 filter 的数组本身。

thisArg 可选参数。执行 callback 时,用于指定 this 的值。

  1. const arr1 = [1, 4, 5, 6, 2, 3, 8, 9, 0];
  2. const arr2 = arr1.filter((item, index, array) => {
  3. return item > 5;
  4. });
  5. console.log(arr2);//>> [6, 8, 9]

在上面的代码中,只要当前的数组项的值大于 5 ,item > 5就会返回true ,则会通过回调函数的测试,从而将该数组项保留,因此将原数组过滤后返回的新数组是[6, 8, 9]

查找方法 find

find() 方法返回数组中通过回调函数测试的第一个数组项的值,如果没有通过测试则返回undefined

语法如下:

  1. var item = arr.find(callback(element[, index[, array]])[, thisArg])

参数说明与上面的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

需要注意的是,一旦回调函数测试通过(返回了 true) ,则 find 方法会立即返回当前数组项item的值;如果没有符合规则的数组项,则会返回undefined

与 find 类似的方法是 findIndex() 方法,区别在于 find 返回元素的值,而 findIndex则返回数组项的下标(索引)。

some

some() 方法测试数组中是不是有数组项通过了回调函数的测试,返回一个Boolean类型的值。

语法如下:

  1. arr.some(callback(element[, index[, array]])[, thisArg])

参数说明与上面的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

与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

在上面的例子中,虽然arr.length为 4 ,但是回调函数只在索引为 0 的项上被调用了,后面的三项由于未被赋值,所以不调用回调函数。

sort

sort() 方法用原地算法 对数组项进行排序,并返回数组。默认排序顺序是在将数组项转换为字符串,然后比较它们的UTF-16代码单元值。

原地算法 (in-place algorithm)是一种使用小的、固定数量的额外空间来转换资料的算法,不随着算法执行而逐渐扩大额外空间的占用。当算法执行时,输入的资料通常会被要输出的部份覆盖掉。

由于排序取决于具体实现,因此无法保证排序的时间和空间复杂度。更多排序算法,可参考「贰.1.1 十大排序算法」。

语法如下:

  1. var newArray = arr.sort([compareFunction]);

参数说明:

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]

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. }

将上面的代码精简下,就会变成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]

上面的例子中, sort 没传入比较的回调函数,它会根据每个数组项的第一个字符进行排序,由于在 Unicode 中,1 在 2 之前,所以 10 会排在 2 之前,而不是根据数字 10 和 2 的大小来比较。如果两个数组项的第一个字符相同,则根据第二个字符对比排序。

reduce

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

语法如下:

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

参数说明:

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: '馆长',
  5. email: 'guanzhang@coffe1891.com'
  6. },
  7. {
  8. username: 'xiaoer',
  9. displayname: '小二',
  10. email: 'xiaoer@coffe1891.com'
  11. },
  12. {
  13. username: 'zhanggui',
  14. displayname: '掌柜',
  15. email: null
  16. },
  17. ];

在有些情况下,我们需要通过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": "guanzhang@coffe1891.com"
  13. // },
  14. // "xiaoer": {
  15. // "username": "xiaoer",
  16. // "displayname": "小二",
  17. // "email": "xiaoer@coffe1891.com"
  18. // },
  19. // "zhanggui":{
  20. // "username": "zhanggui",
  21. // "displayname": "掌柜",
  22. // "email": null
  23. // }
  24. // }

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. function callback(acc, line) {
  9. return acc.concat(line.split(/,/g));
  10. }
  11. const arr1 = arr.reduce(callback, []);
  12. console.log(arr1);
  13. //>> [
  14. // "Algar",
  15. // "Bardle",
  16. // "Mr. Barker",
  17. // "Barton",
  18. // "Baynes",
  19. // "Bradstreet",
  20. // "Sam Brown",
  21. // "Monsieur Dubugue",
  22. // "Birdy Edwards",
  23. // "Forbes",
  24. // "Forrester",
  25. // "Gregory",
  26. // "Tobias Gregson",
  27. // "Hill",
  28. // "Stanley Hopkins",
  29. // "Athelney Jones"
  30. // ]

上面代码把一个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}

这种方法需要遍历两次数组。但是,现在有了一种不需要遍历次数这么多的方法。自从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}

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

假设我们有一个跟上文相同的peopleArr数组。我们现在要找出最近的登陆用户,并且去掉没有email地址的。一般情况下,我们通常使用下面这三个步骤的方法:

  1. 过滤掉所有没有email的对象;
  2. 提取最近的对象;
  3. 找出最大值。

放在一起我们可以得到如下代码:

  1. function notEmptyEmail(x) {
  2. return (x.email !== null) && (x.email !== undefined);
  3. }
  4. function getLastSeen(x) {
  5. return x.lastSeen;
  6. }
  7. function greater(a, b) {
  8. return (a > b) ? a : b;
  9. }
  10. const peopleWithEmail = peopleArr.filter(notEmptyEmail);
  11. const lastSeenDates = peopleWithEmail.map(getLastSeen);
  12. const mostRecent = lastSeenDates.reduce(greater, '');
  13. console.log(mostRecent);
  14. //>> 2019-05-13T11:07:22+00:00

以上代码既兼顾了功能也拥有良好的可读性,同时对于简单的数据,可以运行良好。但是如果我们有一个巨大的数组,我们就有可能会碰上内存问题了。这是因为我们使用变量去储存了每一个中间数组。如果我们对reducer callback方法做一些改动,我们就可以一次性完成以上三步工作了。

  1. function notEmptyEmail(x) {
  2. return (x.email !== null) && (x.email !== undefined);
  3. }
  4. function greater(a, b) {
  5. return (a > b) ? a : b;
  6. }
  7. function notEmptyMostRecent(currentRecent, person) {
  8. return (notEmptyEmail(person))
  9. ? greater(currentRecent, person.lastSeen)
  10. : currentRecent;
  11. }
  12. let result = peopleArr.reduce(notEmptyMostRecent, '');
  13. console.log(result);
  14. //>> 2019-05-13T11:07:22+00:00

以上使用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. function getUsername(person) {
  6. return person.username;
  7. }
  8. async function chainedFetchMessages(p, username) {
  9. // 在这个函数体内, p 是一个promise对象,等待它执行完毕,
  10. // 然后运行 fetchMessages().
  11. const obj = await p;
  12. const data = await fetchMessages(username);
  13. return { ...obj, [username]: data};
  14. }
  15. const msgObj = peopleArr
  16. .map(getUsername)
  17. .reduce(chainedFetchMessages, Promise.resolve({}))
  18. .then(console.log);
  19. //>> {glestrade: [ … ], mholmes: [ … ], iadler: [ … ]}

注意这段代码的逻辑,我们必须通过promise.resolve()调用promise回调函数,作为reducer的初始值。它会立刻调用resolve方法,这样一连串的API请求就开始接连运行了。

解压密码: detechn或detechn.com

免责声明

本站所有资源出自互联网收集整理,本站不参与制作,如果侵犯了您的合法权益,请联系本站我们会及时删除。

本站发布资源来源于互联网,可能存在水印或者引流等信息,请用户自行鉴别,做一个有主见和判断力的用户。

本站资源仅供研究、学习交流之用,若使用商业用途,请购买正版授权,否则产生的一切后果将由下载用户自行承担。

正则表达式
« 上一篇 11-16
彻底搞懂 this
下一篇 » 11-16

发表评论