JavaScript数据结构之数组

几乎所有的编程语言都原生支持数组类型,因为其是最简单的内存数据结构。数组也是 JavaScript 中最常见的数据结构之一,它提供了很多处理存储数据的方法。JavaScript 中,数组是经过改进的对象,和其他语言不同的是,数组中每个槽位可以存储任意类型的数据,这意味着可以创建一个数组,它的第一个元素是字符串、第二个元素是数字、第三个是对象。在 JavaScript 中拥有许多很实用的方法,本文就来总结一下数组中常用的操作方法。

1. Array.map()

使用.map() 方法,可以创建一个基于原始数组的修订版数组。.map() 方法接受一个函数,该函数遍历数组中的所有项并进行相应的修改。

语法

const newArray = array.map(function callback(currentValue[, index[, array]]) {
 // 为新数组返回新的元素
}[, thisArg])

map 函数用于遍历数组元素。它接受一个回调函数作为参数,根据回调函数返回一个新数组和新元素。

参数

  • callback(必须):生成新数组元素的函数,接收三个参数:

    • currentValuecallback 数组中正在处理的当前元素。
    • index:可选,callback 数组中正在处理的当前元素的索引。
    • array:可选,map 方法调用的数组。
  • thisArg:可选,执行 callback 函数时值被用作 this

返回值

一个由原数组每个元素执行回调函数的结果组成的新数组。

当需要更新数组中的所有项并将其存储到一个新数组中时,.map() 方法就可以派上用场了。

实例

例如有一个文章列表的数组,如下:

const articles = [
    {
        article_id: "6976209276364652558",
        title: "如何在 Vue 的计算属性中传递参数",
        views: 1258,
    },
    {
        article_id: "6976113133358153736",
        title: "Angular数据状态管理框架:NgRx/Store",
        views: 2258,
    },
    {
        article_id: "6975722363241365534",
        title: "Angular管道PIPE介绍",
        views: 1568,
    },
];

现在基于上面文章列表数组,获取所有 title 组成的数组,如下:

const arrayTitles = articles.map((item) => item.title);
console.log(arrayTitles);

输出的结果如下:

[
  '如何在 Vue 的计算属性中传递参数',
  'Angular数据状态管理框架:NgRx/Store',
  'Angular管道PIPE介绍'
]

当然,只要是数组都可以使用 .map() ,接下来就基于上面标题数组,增加作者信息,如下:

const arrayTitlesWithAuthor = arrayTitles.map(
    (title) => `《${title}》作者:天行无忌`
);
console.log(arrayTitlesWithAuthor);

输出结果如下:

[
  '《如何在 Vue 的计算属性中传递参数》作者:天行无忌',
  '《Angular数据状态管理框架:NgRx/Store》作者:天行无忌',
  '《Angular管道PIPE介绍》作者:天行无忌'
]

.map() 方法是一个用来创建新数组、修改其内容并保持原始数组不变的通用方法。当出现需要修改现有数组的内容并将结果存储为新变量的时候就可以用。

2. Array.filter()

从方法名称可以很容易知道其用途,没错用于过滤数组元素。

filter()方法根据特定条件获取数组中的元素,像 .map() 方法一样,它将返回一个新数组,并保持原始数组不变。

语法

const newArray = array.filter(callback(element[, index[, array]])[, thisArg])
  • callback :用来测试数组的每个元素的回调函数,返回 true 表示该元素通过测试,保留该元素,false 则不保留。它接受以下三个参数:
    • element:数组中当前正在处理的元素。
    • index:可选,正在处理的元素在数组中的索引。
    • array:可选,调用了 filter 的数组本身。
  • thisArg:可选,执行 callback 时,用于 this 的值

返回值

一个新的、由通过测试的元素组成的数组,如果没有任何数组元素通过测试,则返回空数组。

实例

基于上面的 articles 数组,分别获取 views 小于 2000 的和大于 2000 的文章列表:

const lessArticles = articles.filter((item) => item.views < 2000);
const muchArticles = articles.filter((item) => item.views > 2000);
console.log(lessArticles);
console.log("\r\n==========================================\r\n");
console.log(muchArticles);

输出的结果如下:

[
  {
    article_id: '6976209276364652558',
    title: '如何在 Vue 的计算属性中传递参数',
    views: 1258
  },
  {
    article_id: '6975722363241365534',
    title: 'Angular管道PIPE介绍',
    views: 1568
  }
]

==========================================

[
  {
    article_id: '6976113133358153736',
    title: 'Angular数据状态管理框架:NgRx/Store',
    views: 2258
  }
]

当需要从数组中删除不符合特定条件的元素时,可以使用 .filter()

3. Array.find()

.find() 方法看起来很像前面介绍的.filter()方法。跟 .filter()方法一样,将匹配的条件的元素返回,区别在于,.find() 将只返回与提供的条件匹配的第一个元素,不是数组。

语法

arr.find(callback[, thisArg])
  • callback:在数组每一项上执行的函数,接收 3 个参数:
    • element:当前遍历到的元素。
    • index:可选,当前遍历到的索引。
    • array:可选数组本身。
  • thisArg:可选执行回调时用作 this 的对象。

返回值

数组中第一个满足所提供测试函数的元素的值,否则返回 undefined

实例

将上面的 .filter() 方法改为 .find(),如下:

const lessArticle = articles.find((item) => item.views < 2000);
const muchArticle = articles.find((item) => item.views > 2000);
console.log(lessArticle);
console.log("\r\n==========================================\r\n");
console.log(muchArticle);

输出结果如下:

{
  article_id: '6976209276364652558',
  title: '如何在 Vue 的计算属性中传递参数',
  views: 1258
}

==========================================

{
  article_id: '6976113133358153736',
  title: 'Angular数据状态管理框架:NgRx/Store',
  views: 2258
}

什么时候使用 Array.find() ? 当需要获取数组通过定义条件的第一个元素时。

4. Array.findIndex()

.findIndex() 方法在名称上跟 .find() 前半部分一样,其实现的功能和.find() 一样,其区别在于返回值不一样,只返回与提供的条件匹配的第一个元素的索引值。

const lessArticle = articles.findIndex((item) => item.views < 2000);
const muchArticle = articles.findIndex((item) => item.views > 2000);
console.log(lessArticle); // 0
console.log(muchArticle); // 1

什么时候使用 Array.findIndex() ? 当需要获取数组通过定义条件的第一个元素所在数组中的索引值时。

5. Array.forEach()

.forEach() 方法的工作方式很像常规的 for 循环,遍历一个数组并在每个元素上执行一个函数。.forEach() 的第一个参数是回调函数,它包含循环数组的当前值和索引。

如下:

articles.forEach((item, index) => {
    console.log(`文章索引 ${index} 的标题为《${item.title}》`);
});

输出结果如下:

文章索引 0 的标题为《如何在 Vue 的计算属性中传递参数》
文章索引 1 的标题为《Angular数据状态管理框架:NgRx/Store》
文章索引 2 的标题为《Angular管道PIPE介绍》

当需要简单地循环遍历数组的每个元素而不需要构建新数组时。

6. for...of

for...of 是es6推出的迭代器,号称最简洁,可以是用 breakcontinuereturn 终止循环。跟 .forEach() 不同的是,不提供数组索引。跟 for 语句相比代码少得多,更简洁。

下面代码遍历输出数组,如下:

for (const item of articles) {
    console.log(item);
}

7. for...in

这个方法跟上面的for...of 语法上看起来相似,for...of 是对值的遍历,for...in 是对 key/index 的遍历。for...in 应用于数组则 key 对应的就是数组的索引值,应用于对象则 key 对应键值。

来看代码执行效果,先应用于数组,如下:

for (const key in articles) {
    console.log(key);
}

上面代码输出的是数组的索引值:0、1、2,下面应用于数组第一个对象,如下:

for (const key in articles[0]) {
    console.log(key);
}

输出的就是:article_idtitleviews

在实际开发中不提倡使用 for...in,如果需要遍历对象属性,推荐使用Object.keys

8. Array.every()

.every()方法将检查数组中的每个元素是否都通过提供的条件,如果数组中的所有元素都通过条件,则将返回 true ,如果没有,将返回 false

语法

arr.every(callback(element[, index[, array]])[, thisArg])
  • callback:用来测试每个元素的函数,它可以接收三个参数:

    • element:用于测试的当前值。
    • index:可选,用于测试的当前值的索引。
    • array:可选,调用 every 的当前数组。
  • thisArg:执行 callback 时使用的 this 值。

返回值

如果回调函数的每一次返回都为 true 值,返回 true ,否则返回 false,即跟逻辑判断的 && 类似,所有结果为真才为真。

实例

例如,检查 articles 数组所有的文章 views 都超过 1000,如下:

const isMoreThan1000 = articles.every((item) => item.views > 1000);
console.log(isMoreThan1000);  // true

检查 articles 数组所有的文章 views 都超过 2000,如下:

const isMoreThan2000 = articles.every((item) => item.views > 2000);
console.log(isMoreThan2000); // false

什么时候使用 Array.every() ? 当需要确认数组的每一项都通过定义的条件时。

9. Array.some()

.some() 方法和 .every() 方法类似,但验证的结果是相反的,如果数组中的所有元素只要有一个通过条件,则将返回 true ,如果所有的元素都不通过条件,将返回 false

.some() 方法和 .every() 方法在一些逻辑处理中,可以实现现逻辑 andor

语法

arr.some(callback(element[, index[, array]])[, thisArg])
  • callback:用来测试每个元素的函数,它可以接收三个参数:

    • element:用于测试的当前值。
    • index:可选,用于测试的当前值的索引。
    • array:可选,调用 some 的当前数组。
  • thisArg:执行 callback 时使用的 this 值。

返回值

数组中有至少一个元素通过回调函数的测试就会返回 true,所有元素都没有通过回调函数的测试返回值才会为 false。即跟逻辑判断的 || 类似,所有结果为假才为假。

实例

例如,检查 articles 数组所有的文章 views 是否有 views 超过 2000 的文章,如下:

const isMore2000 = articles.some((item) => item.views > 2000);
console.log(isMore2000); // true

检查 articles 数组所有的文章 是否有 views 超过 3000 的文章,如下:

const isMore3000 = articles.some((item) => item.views > 3000);
console.log(isMore3000);  // false

10. Array.reduce()

reduce() 方法对数组中的每个元素执行 reducer 函数(升序执行),将其结果汇总为单个返回值。

语法

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

实现了为数组中的每一个元素依次执行 callback 函数,不包括数组中被删除或从未被赋值的元素。

参数

  • callback(必须):执行数组中每个值 (如果没有提供 initialValue 则第一个值除外)的reducer函数,包含四个参数

  • initialValue(可选):作为第一次调用 callback 函数时的第一个参数的值。** 如果没有提供初始值,则将使用数组中的第一个元素**。 在没有初始值的空数组上调用 reduce 将报错。

返回值

函数累计处理的结果。

.reduce() 方法接受一个回调函数作为其第一个参数,一个可选的初始值作为其第二个参数。如果没有提供初始值,则使用第一个数组元素作为值。回调函数提供一个累加器 accumulatorcurrentValue 参数,用于执行 reduce 计算。

实例

const arrNumbers = [1, 2, 3, 4, 5];
const reduceNumbers = (arrayNumbers, accumulatorInitVal = false) => {
    const reduceCallback = (accumulator, currentVal, currentIndex) => {
        console.log(`当前索引:${currentIndex}`);
        return accumulator + currentVal;
    };
    return accumulatorInitVal
        ? arrayNumbers.reduce(reduceCallback, accumulatorInitVal)
        : arrayNumbers.reduce(reduceCallback);
};

console.log(reduceNumbers(arrNumbers)); // 15,累计器初始值为数组的第一个元素的值1
console.log(reduceNumbers(arrNumbers, 10)); // 25,累计器初始值为10

console.log (当前索引:${currentIndex}),是为了更加直观的看到索引值。

第一次未定义初始值输出如下:

当前索引:1
当前索引:2
当前索引:3
当前索引:4

第二次定义了累计器初始值输出如下:

当前索引:0
当前索引:1
当前索引:2
当前索引:3
当前索引:4

先来一个简单的实例,对 articles 数组的 views 进行累加求和:

const sumViews = articles.reduce(
    (accumulator, current) => accumulator + current.views,
    0
);
console.log(sumViews); // 5084

使用 .reduce() 方法可以用于展平一个数组,当然已经有很多方法可以做到这一点,这就是其中的方法之一。

const flattened = [
    [0, 1],
    [2, 3],
    [4, 5],
].reduce((accumulator, current) => accumulator.concat(current), []);
console.log(flattened); // [ 0, 1, 2, 3, 4, 5 ]

当需要通过操作其值将数组向下转换为单个值时,可以使用 .reduce() 方法

接下来我们来看一个奇葩需求,出于某种原因,需要一个包含所有用户全名的新数组(他们的姓,加上他们的名字),但只有当他们是 20 多岁,并且他们的全名是 3 个字的时候才需要。不要问我们为什么需要这么奇葩的数据子集,产品经理问了,我们很乐意帮忙^_^

const users = [
    {
        firstName: "坚",
        lastName: "孙",
        age: 37,
    },
    {
        firstName: "策",
        lastName: "孙",
        age: 21,
    },
    {
        firstName: "葛亮",
        lastName: "诸",
        age: 28,
    },
    {
        firstName: "备",
        lastName: "刘",
        age: 44,
    },
    {
        firstName: "统",
        lastName: "庞",
        age: 22,
    },
    {
        firstName: "维",
        lastName: "姜",
        age: 19,
    },
    {
        firstName: "伯温",
        lastName: "刘",
        age: 22,
    },
];
const getFullName = (user) => `${user.lastName}${user.firstName}`;
const filterByAge = (user) => user.age >= 20 && user.age < 30;

// 常规实现
const getFilterResult = users
    //  第一步筛选年龄20-30之间的用户
    .filter((user) => filterByAge(user))
    //  拼接全名
    .map((user) => getFullName(user))
    //  筛选
    .filter((fullName) => fullName.length === 3);

console.log(getFilterResult);   // [ '诸葛亮', '刘伯温' ]

// 迭代方式实现
const iterationsFilterResult = (arrayResult, currentUser) => {
    const fullname = getFullName(currentUser);
    if (filterByAge(currentUser) && fullname.length === 3) {
        arrayResult.push(fullname);
    }
    return arrayResult;
};
console.log(users.reduce(iterationsFilterResult, []));  // [ '诸葛亮', '刘伯温' ]

11. Array.slice()

slice() 方法将数组部分的副本返回到新的数组对象中。这个对象是从 startend 选择的。需要注意的是,此方法不会修改原始数组。此外,如果向其中一个数组添加新元素,则另一个数组不会受到影响。

语法

arr.slice([begin[, end]])

slice() 方法的参数是数组的开始和结束索引。

  • start:是一个从 0 开始的索引,用于开始复制数组的一部分。如果未定义,start 的默认值为 0。如果 start 大于数组的索引范围, slice() 方法将返回一个空数组。此外,start 还可以使用负索引。 slice(-1) 提取数组的最后一个元素。
  • end:可选,如果 slice() 函数中只有一个参数,那就是 start。如果省略, slice() 方法从数组的末尾开始提取。如果 end 大于数组的长度,slice() 一直提取到数组的末尾,只是在它被省略的情况下。end 是提取此索引之前的元素,不包括索引 end 在内。因此,索引的最后一个元素不包含在数组的副本中。例如,slice(1,3) 提取是数组的第二个和第三个元素,即从数组的索引 1 开始,包含索引 1 的值到索引 3 之间的数组,但不包含索引为 3 的元素。

返回值

一个含有被提取元素的新数组。

实例

下面实例代码是复制数组 arrNumbers 从索引 0 开始到索引 3 之间的元素,不包含索引为 3 的元素 。

const arrNumbers = [1,3,5,6,7];
console.log(arrNumbers.slice(0, 3)); // [ 1, 3, 5 ]

可以用于复制数组,上面示例是slice() 函数的基本功能,没有参数的数组复制原始数组。有时,可能想要更新数组中的某些元素。但是,可能需要保护原始数组中的元素,可以使用 slice() 创建原始数组的浅复制

const arrNumbers = [1, 2, 3, 4, 5, 6];
const copyNumbers = arrNumbers.slice();
console.log(copyNumbers); // [ 1, 2, 3, 4, 5, 6 ]
copyNumbers[1] = 0;
console.log(copyNumbers); // [ 1, 0, 3, 4, 5, 6 ]
console.log(arrNumbers); // [ 1, 2, 3, 4, 5, 6 ]

12. Array.splice()

splice() 方法可以从数组中排除旧值,然后将新值插入数组。使用 splice() 后,将获得两个数组,第一个是排除的数组,第二个是编辑过的新数组。 需要注意的是,此方法会改变原数组

语法

splice(start[, deleteCount[, item1[, item2[, ...]]]]) 方法的参数是从数组 start 索引开始,删除 deleteCount 个元素,返回删除元素组成的数组

  • start: 是一个从 0 开始的索引,用于指定修改的开始索引位置。如果未定义,start 的默认值为 0。如果 start 大于数组的索引范围,则从数组末尾开始添加内容。此外,start 还可以使用负索引,则表示从数组末位开始的第几位(从 -1 计数,这意味着 -n 就是倒数第 n 个元素,其实就是等价于 array.length-n

  • deleteCount:是可选的,整数,表示要移除的数组元素的个数。如果 deleteCount 大于 start 之后的元素的总数,则从 start 后面的元素都将被删除(含第 start 位)。如果 deleteCount 被省略了,或者它的值大于等于 array.length - start(也就是说,如果它大于或者等于 start 之后的所有元素的数),那么 start 之后数组的所有元素都会被删除。如果 deleteCount0 或者负数,则不移除元素,这种情况下,至少应添加一个新元素。

  • tem1, item2, ... :可选,要添加进数组的元素,从 start 位置开始。如果不指定,splice() 则将只删除数组元素。添加进去的元素的位置位于开始删除索引后,结束于结束索引前。

返回值

由被删除的元素组成的一个数组。如果只删除了一个元素,则返回只包含一个元素的数组。如果没有删除元素,则返回空数组。

实例

const arrNumbers = [1, 3, 5, 6, 7];
console.log(arrNumbers.splice(0, 3, 8, 9)); // [ 1, 3, 5 ]
console.log(arrNumbers); // [ 8, 9, 6, 7 ]

// 删除元素
const arrNumbers = [1, 3, 5, 6, 7];
console.log(arrNumbers.splice(0)); // [ 1, 3, 5, 6, 7 ]
console.log(arrNumbers); // [] 

// 在数组中插入元素
const ColorNames = (start, deleteCount, arr) => {
    arr.splice(start, deleteCount, "Pink", "Black", "Green");
    return arr;
};
// 数组后
console.log(ColorNames(2, 0, ["Golden", "Brown"])); // [ 'Golden', 'Brown', 'Pink', 'Black', 'Green' ]
// 数组前
console.log(ColorNames(0, 0, ["Golden", "Brown"])); // [ 'Pink', 'Black', 'Green', 'Golden', 'Brown' ]

13. Array.sort()

.sort() 对数组进行排序,不考虑其数据类型:无论是编号数组、字符串数组还是复杂的对象数组。需要注意的一点是,.sort() 方法不会返回新的排序数组,而是更改原始数组

语法

arr.sort([compareFunction])
  • compareFunction:可选,用来指定按某种顺序进行排列的函数。如果省略,元素按照转换为的字符串的各个字符的 Unicode 位点进行排序。
    • firstEl:第一个用于比较的元素。
    • secondEl:第二个用于比较的元素。

对于 compareFunction ,返回值的不同将影响数组的排序,如下:

  • 如果 compareFunction(a, b) 返回 0,则表示 a 排在 b 前面
  • 如果compareFunction(a, b) 返回大于 0 的值,则表示 b 排在 a 前面
  • 如果 compareFunction(a, b) 返回小于 0 的值 ,则表示 a 排在 b 前面

返回值

排序后的数组,不会返回新的排序数组,而是更改原始数组。

实例

const months = ["Nov", "Feb", "Jan", "Dec"];
const sorted = months.sort();

console.log(months); // [ 'Dec', 'Feb', 'Jan', 'Nov' ]
console.log(sorted); // [ 'Dec', 'Feb', 'Jan', 'Nov' ]

总结

JavaScript 提供了大量不同的处理数组的方法,本文介绍的内容基本囊括了可能用到的数组方法,而对于.concat() 方法,几乎用扩展符替代了,如 [...array1,...array2] 。数组操作是编程世界里面最基本的操作,有必要熟练掌握。