likes
comments
collection
share

『 纯干货』帮你梳理总结眼花缭乱的数组 API

作者站长头像
站长
· 阅读数 13

前言

数组 作为一个最基础的一维数据结构,在各种编程语言中都充当着至关重要的角色,很难想象如果没有数组的编程语言会是什么样的。尤其是 JavaScript,它天生的灵活性又近一步发挥了 数组 的特长,丰富了 数组 的使用场景,可以毫不夸张的说,如果不深入地了解数组,就不足以写好 JavaScript

在当下我们使用的框架中,例如:ReactVueMVVM 框架,它们内部数据更新的同时视图也会随之更新。通过这些框架实现的大量业务代码中,开发者都会用数组来进行数据的存储和各种操作 -- "增删查改",从而实现对应前端视图层的更新。

由此可见熟练的掌握数组的操作方法,并深入理解数组是很有必要的,因此今天我们就一起来盘一下 JavaScript 中数组相关的 API,加深对数组的学习和理解,让我们开始吧!

带着问题来学习

前面的章节一样,我们学习知识时,还是先带着问题出发,当我们定下一个学习目标后,学习的效率就能达到事半功倍,下面我们一起来看一下相关的问题吧,如下:

  1. 数组的构造器方法有哪几种?
  2. 数组中哪些方法会改变自身?
  3. 数组中哪些方法不会改变自身?
  4. 数组中的遍历方法有哪些?

带着上述的问题,在学习的过程中就可以有目标且针对性的理解和记忆。

截至 ES7 规范,JavaScript 中数组总共包含33个标准的API方法和一个非标准的 API 方法,使用场景和使用方案纷繁复杂,其中还有不少坑,为了加深对数组的学习和理解,我们从最基础的内容开始学起。由于数组的API较多,很多相近的名字也会导致混淆,因此我们将进行分类学习,按照如下分类进行。

  • 会改变自身值的方法
  • 不会改变自身值的方法
  • 遍历数组的方法

根据上述的分类,让我们对这些API形成更结构的认识。

Array构造器

Array构造器通常用于创建一个新的数组,一般推荐使用 对象字面量 的方式创建数组,代码如下:

// 使用 Array 构造器,可以自定义长度
const a = Array(6);

// 使用对象字面量
const b = [];
b.length = 6;

上述代码中,通过 Array 构造器创建的数组长度为6,而通过对象字面量创建的数组是无法直接指定长度的,需要通过它上面的 length 属性指定数组的长度。

Array 的构造器根据参数长度的不同,有两种不同的处理方式。

  • new Array(arg1, arg2, ...) 参数长度为0或长度大于等于2时,传入的参数将按照顺序依次成为新数组的第0至N项(参数长度为0时,返回空数组)
  • new Array(len) 当len不是数值时,处理同上,返回的是一个只包含len元素的数组;当len为数值时,len最大不能超过32位无符号整型,即需要小于2的32次方(len最大为Math.pow(2, 32)),否则将抛出RangeError异常

ES6 新增的构造方法

ES6 中专门为数组构造器扩展了两个新的方法,它们分别是:Array.ofArray.from,其中 Array.of 整体使用较少,而 Array.from 具有较高的灵活性,在日常的开发中经常会使用到。关于这两个方法的使用细节大家都了解吗?下面我们一起展开来看一下这两个方法。

  • Array.of:主要用于将参数依次转化为数组中的一项,然后返回一个新的数组,但它不会管这个参数是数字还是其他,它与Array构造器的功能基本一致,唯一的区别就是在单个数字参数的处理上不同,如下:
Array.of(8); // [8]
Array(8); // [empty X 8]
  • Array.from:设计初衷是为了快速便捷地基于其他对象创建一个新数组,准确来说就是从一个类似数组的可迭代对象中创建一个新的数组实例。简单来说就是当一个可以是可迭代的,那么 Array.from 就能将它变成数组。Array.from 有三个参数,其中第一个参数为必填的,是一个类似数组的对象;第二个参数是加工函数,新生成的数组会经过该函数的加工再返回;第三个参数是 this 作用于,表示加工函数执行时 this 的值。可以通过代码来看一下具体的用法,如下:
const obj = {0: 'a', 1: 'b', 2: 'c', length: 3};
Array.from(obj, function (value, index) {
    console.log(value, index, this, arguments.length);
    // 必须指定返回值,否则返回undefined
    return value.repeat(3);
}, obj);

上述代码在控制台中执行结果如下:

『 纯干货』帮你梳理总结眼花缭乱的数组 API

通过上面的执行效果可以看到最终的结果返回了三次,是因为我们在最后通过 return 返回出来的。如果我们不指定 this 也可以正常的执行,上述的代码可以修改如下:

Array.from(obj, (value) => value.repeat(3));

最终的执行效果更上述是一样的,只是这里我们没有用到 this ,因此可以直接使用 ES6 中的箭头函数。

除了上面说的对象外面,像 ES6 中的 SetMap 对象也是可迭代的对象,因此它们也可以通过 Array.from 转换为一个数组,具体代码如下:

Array.from('abc'); // ['a', 'b', 'c']

Array.from(new Set(['abc', 'bcd'])); // ['abc', 'bcd']

Array.from(new Map([[1, 'ab'], [2, 'cd']])); // [[1, 'ab'], [2, 'cd']]

Array 的判断

通常我们使用 Array.isArray 来判断一个变量是否为数组类型,在 ES6 提供 Array.isArray 之前,我们至少有五种方式来判断一个变量是否是数组,我们可以一起来看一下相关的代码,如下:

var a = [];
// 基于 instanceof
a instanceof Array;
// 基于 constructor
a.constructor === Array;
// 基于 Object.prototype.isPrototypeOf
Array.prototype.isPrototypeOf(a)
// 基于 getPrototypeOf
Object.getPrototypeOf(a) === Array.peototype;
// 基于 Object.prototype.toString
Object.prototype.toString.apply(a) === '[object Array]';

上述代码中,这五个结果都是 true,这些代码大家都应该能够看懂,都是很简单的代码。当 ES6 添加了 Array.isArray 后,我们要判断一个变量是否是数组就变的极为简单了,但是如果 Array.isArray不存在,那我们可以自己来实现一个 Array.isArray,具体代码如下:

if (!Array.isArray) {
    Array.isArray = function (arg) {
        return Object.prototype.toString.apply(arg) === '[object Array]';
    }
}

上述的内容都是 Array 的构造方法,下面我们一起来看一下 Array30多个令人眼花缭乱的 API

数组中会改变自身的方法

在数组中分为会改变数组本身和不会改变数组本身的方法,其中像 poppushreverseshiftsortspliceunshift 以及两个 ES6 中新增的方法 copyWithinfill,它们都是会改变原数组本身的内容,我们可以一起来看一下相关的代码,如下:

// pop
const plants = ['broccoli', 'cauliflower', 'cabbage', 'kale', 'tomato'];
console.log(plants.pop()); // 'tomato'
console.log(plants); // ["broccoli", "cauliflower", "cabbage", "kale"]

// push
const animals = ['pigs', 'goats', 'sheep'];
const count = animals.push('cows');
console.log(count); // 4
console.log(animals); // ["pigs", "goats", "sheep", "cows"]

// reverse
const array1 = ['one', 'two', 'three'];
console.log('array1:', array1); // ['one', 'two', 'three'];
const reversed = array1.reverse();
console.log('reversed:', reversed); // ["three", "two", "one"]

// shift
const array2 = [1, 2, 3];
const firstElement = array1.shift();
console.log(array2); // [2, 3]
console.log(firstElement); // 1

// sort
const months = ['March', 'Jan', 'Feb', 'Dec'];
months.sort();
console.log(months); // ["Dec", "Feb", "Jan", "March"]

// splice
const months2 = ['Jan', 'March', 'April', 'June'];
months.splice(1, 0, 'Feb');
console.log(months); // ["Jan", "Feb", "March", "April", "June"]

// unshift
const array3 = [1, 2, 3];
console.log(array3.unshift(4, 5)); // 5
console.log(array3); // [4, 5, 1, 2, 3]

// copyWithin
const array4 = ['a', 'b', 'c', 'd', 'e'];
console.log(array4.copyWithin(0, 3, 4)); // ["d", "b", "c", "d", "e"]
console.log(array5.copyWithin(1, 3)); // ["d", "d", "e", "d", "e"]

// fill
const array5 = [1, 2, 3, 4];
console.log(array5.fill(0, 2, 4)); // [1, 2, 0, 0]
console.log(array1.fill(5, 1)); // [1, 5, 5, 5]

上述的示例中,每个方法都会改变原数组的值,这些都是数组中常用的 API,因此需要大家加强对这些 API 的理解和记忆,为了加深印象,我们一起来看一个 leetcood 上面关于数组的真题,题目如下:

给你两个有序整数数组 nums1 和 nums2,请你将 nums2 合并到 nums1 中,使 nums1 成为一个有序数组。

输入:
nums1 = [1, 2, 3, 0, 0, 0]; m = 3
nums2 = [2, 5, 6]; n = 3

输出:
[1, 2, 2, 3, 5, 6]

上述题目中的要求可以概括如下:

  • 首先是将 nums2 合并到 nums1 中,不开新的数组,否则将无法通过
  • 其次当合并完成后,nums1 还是一个有序数组,这也是需要注意的
  • 另外上述的题目中, nums1 和 nums2 里面都有 “2” 和 “3” 这两个相同的数字,因此也需要将这些重复的数字进行合并

通过对题目的分析,我们可以得知不能新开数组,那么就需要用到能够改变原数组的方法来进行解答。该如何实现呢?你可以自己先不看下面的案例,自己先实现以下。

下面让我们一起来看一下最终实现的代码,如下:

const mergeArray = (nums1, m, nums2, n) => {
    nums1.splice(m);
    nums2.splice(n);
    nums1.push(...nums2);
    nums1.sort((a, b) => a - b);
};

通过上述的代码,我们可以看到使用的方法都是能够改变数组自身的方法,最终在 LeetCode 中提交后运行如下:

『 纯干货』帮你梳理总结眼花缭乱的数组 API

上面这部分内容中使用的 API 是能够改变原数组的,下面我们一起来看对数组操作不会改变它本身的方法。

数组中不会改变自身的方法

基于 ES7 不改变数组本身的方法有9个,具体的 API 有:concatjoinslicetoStringtoLocaleStringindexOflastIndexOf,以及未形成标准的 toSourceES7 中新增的 includes 方法,它们都是不会改变原数组本身的内容,我们可以一起来看一下相关的代码,如下:

// concat
const array1 = ['a', 'b', 'c'];
const array2 = ['d', 'e', 'f'];
const array3 = array1.concat(array2);
console.log(array3); // ["a", "b", "c", "d", "e", "f"]
console.log(array1); // ['a', 'b', 'c'];
console.log(array2); // ['d', 'e', 'f'];

// join
const elements = ['Fire', 'Air', 'Water'];
console.log(elements.join('-')); // "Fire-Air-Water"
console.log(elements); // ['Fire', 'Air', 'Water']

// slice
const animals = ['ant', 'bison', 'camel', 'duck', 'elephant'];
console.log(animals.slice(2)); // ["camel", "duck", "elephant"]
console.log(animals.slice(2, 4)); // ["camel", "duck"]
console.log(animals); // ['ant', 'bison', 'camel', 'duck', 'elephant']

// toString
const array1 = [1, 2, 'a', '1a'];
console.log(array1.toString()); // "1,2,a,1a"
console.log(array1); // [1, 2, 'a', '1a']

// toLocaleString
const array4 = [1, 'a', new Date('21 Dec 1997 14:12:00 UTC')];
const localeString = array4.toLocaleString('en', { timeZone: 'UTC' });
console.log(localeString); // "1,a,12/21/1997, 2:12:00 PM"
console.log(array4); // [1, 'a', new Date('21 Dec 1997 14:12:00 UTC')]

// indexOf
const beasts = ['ant', 'bison', 'camel', 'duck', 'bison'];
console.log(beasts.indexOf('bison')); // 1
console.log(beasts.indexOf('bison', 2)); // 4
console.log(beasts.indexOf('giraffe')); // -1
console.log(beasts); ['ant', 'bison', 'camel', 'duck', 'bison']

// lastIndexOf
const animals2 = ['Dodo', 'Tiger', 'Penguin', 'Dodo'];
console.log(animals2.lastIndexOf('Dodo')); // 3
console.log(animals2.lastIndexOf('Tiger')); // 1
console.log(animals2); // ['Dodo', 'Tiger', 'Penguin', 'Dodo']

// includes
const pets = ['cat', 'dog', 'bat'];
console.log(pets.includes('cat')); // true
console.log(pets.includes('at')); // false
console.log(pets); // ['cat', 'dog', 'bat']

上述代码中,它们执行都不会改变自身,尤为需要注意是,slice 不会改变数组自身,而 splice 则会改变自身,并且这两个单词比较相近,因此我们在学习和记忆的过程中不要将它们两个给搞混淆了。它们两个的使用方式以及参数都有很大的不同,我们可以一起来看一下对比,代码如下:

arr.slice([start[, end]]);

arr.splice(start, deleteCount[, item1[, item2[, ...]]])

其中 splice 的第二个参数是要删除元素的个数,而 slice 的第二个参数的数组的结束位置。

除此之外,indexOflastIndexOf 的基本功能也是差不多的,唯一的区别是 lastIndexOf 是从数组的末尾开始往前寻找元素的下标。

至于 toSource 方法,目前还未形成标准,这里只需要知道有这个方法就行了。

上面这部分内容中使用的 API 是不会改变原数组的,下面我们再一起来看一下数组中的遍历方法有哪些吧。

数组遍历的方法

ES5 中数组的遍历方法有7个,以及 ES6 中新增的5个,总共是12个遍历的方法,它们的 API 分别是:forEacheverysomefiltermapreducereduceRightentriesfindfindIndexkeysvalues,其中后面5个方法是 ES6 中新增的。和上面一样,我们还是先看一下相关的代码,对每个方法先有一个初步的认识,代码如下:

// forEach
const array1 = ['a', 'b', 'c'];
array1.forEach(element => console.log(element)); // 执行三次,分别输出:"a", "b", "c"

// every
const isBelowThreshold = (currentValue) => currentValue < 40;
const array2 = [1, 30, 39, 29, 10, 13];
console.log(array2.every(isBelowThreshold)); // 判断数组内的所有制都小于40,最终执行结果返回 true

// some
const array3 = [1, 2, 3, 4, 5];
const even = (element) => element % 2 === 0;
console.log(array3.some(even)); // 判断数组中至少有一个数字能被2整除,最终执行结果返回 true

// filter
const words = ['spray', 'limit', 'elite', 'exuberant', 'destruction', 'present'];
const result = words.filter(word => word.length > 6);
console.log(result); // 筛选数组中长度大于6位的元素,最终执行结果返回 ["exuberant", "destruction", "present"]

// map
const array4 = [1, 4, 9, 16];
const map1 = array4.map(x => x * 2);
console.log(map1); // 遍历数组中每一项,最终执行结果返回 [2, 8, 18, 32]

// reduce
const array5 = [1, 2, 3, 4];
// 0 + 1 + 2 + 3 + 4
const initialValue = 0;
// reduce 方法相对来说比较复杂,其中它有两个参数,具体可以参考 MDN 官方文档
const sumWithInitial = array5.reduce(
  (previousValue, currentValue) => previousValue + currentValue,
  initialValue
);
console.log(sumWithInitial); // 最终执行结果返回 10

// reduceRight
const array6 = [[0, 1], [2, 3], [4, 5]];
const result = array6.reduceRight((accumulator, currentValue) => accumulator.concat(currentValue));
console.log(result); // [4, 5, 2, 3, 0, 1]

// entries
const array7 = ['a', 'b', 'c'];
const iterator1 = array7.entries();
console.log(iterator1.next().value); // [0, "a"] 
console.log(iterator1.next().value); // [1, "b"]

// find
const array8 = [5, 12, 8, 130, 44];
const found = array8.find(element => element > 10);
console.log(found); // 12

// findIndex
const array9 = [5, 12, 8, 130, 44];
const isLargeNumber = (element) => element > 13;
console.log(array9.findIndex(isLargeNumber)); // 3

// keys
[...Array(10).keys()]; // [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
[...new Array(10).keys()]; // [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

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

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

上述代码中,我们需要注意的是,有些方法是不会返回处理后的数组,例如:forEach;而有些方法是会返回处理后的数组,例如:filter。这些细节是我们需要注意的地方。

上述的方法中,我们尤为需要特别关注的方法就是 reduce,由于它本身的参数繁多,导致理解起来会较为困难,但在实际的开发中,它能帮我们有效的解决一些疑难问题,下面我们一起来看一个真实的问题,如下:

有一个数组对象 const arr = [{name: 'bar1'}, {name: 'bar2'}, {name: 'bar3'}]
希望通过计算后,最终能够返回的 arr 数组对象里面中的 name 能够拼接成 'bar1,bar2&bar3'
如何用 reduce 来实现呢?

让我们一起来看一下 reduce 是如何结果这个问题的,代码如下:

const arr = [{name: 'bar1'}, {name: 'bar2'}, {name: 'bar3'}];
arr.reduce((prev, current, index, array) => {
    if (index === 0) {
        return current.name;
    } else if (index === array.length - 1) {
        return prev + '&' + current.name;
    } else {
        return prev + ',' + current.name;
    }
}, '');
// 最终返回的结果是 'bar1,bar2&bar3'

通过上述代码的演示,应该就能够理解 reduce 的操作了,这里使用 reduce 可以简便的帮助我们实现数据的累加。

最后

通过将数组的方法进行分门别类,我们知道了哪些方法会改变数组自身,哪些方法不会改变,并且学习了数组中相关的遍历方法,那么在最初留下的思考题现在知道该如何解答了吗?我们可以总结为以下的表格,方便我们后续加深记忆。

数组分类/标准改变自身的方法不改变自身的方法遍历方法(不改变自身)
ES5 及以前pop、push、reverse、shift、sort、splice、unshiftconcat、join、slice、toString、toLocaleStringforEach、every、some、filter、map、reduce、reduceRight
ES6 / 7 / 8copyWithin、fillincludes、toSourceentries、find、findIndex、keys、values

最后,如果这篇文章有帮助到你,❤️关注+点赞❤️鼓励一下作者,谢谢大家

参考文档

MDN Array

往期回顾

如何实现 new、apply、call、bind ?这篇文章告诉你

JS 中这些继承方式你知道吗?

箭头函数能作为事件监听的回调函数吗?

用操作数组的方式来操作对象,该怎么做?

这几个数组的操作方法你必须知道