一、 核心迭代方法:forEach、map、filter、reduce
这些是函数式编程的基石,虽然 forEach、map、filter 在 ES5 中就已存在,但它们在 ES6 的编程范式中扮演着核心角色,我们在此一并重温并强调其重要性。
1. forEach - 单纯的迭代器
forEach 是最简单的方法,它仅仅为数组中的每个元素执行一次提供的函数,没有返回值(返回 undefined)。
场景:当你需要遍历数组并执行一些副作用(如修改外部变量、打印日志、操作 DOM)时。
javascript
// ES5 及之前
var arr = [1, 2, 3];
for (var i = 0; i < arr.length; i++) {
console.log(arr[i]);
}
// ES6+
const arr = [1, 2, 3];
arr.forEach(item => console.log(item));
// 输出: 1, 2, 3
注意:forEach 无法中止或跳过(除了抛出异常),如需中止,请使用 for...of 循环。
2. map - 数组变形专家
map 是使用频率最高的方法之一。它会遍历数组,对每个元素应用一个函数,并将结果收集到一个新数组中。原数组保持不变。
场景:将一种类型的数组转换为另一种类型的数组。
javascript
const numbers = [1, 2, 3, 4];
// 将每个数字转换为它的平方
const squares = numbers.map(num => num * num);
console.log(squares); // [1, 4, 9, 16]
// 从对象数组中提取特定属性
const users = [{name: 'Alice'}, {name: 'Bob'}, {name: 'Charlie'}];
const names = users.map(user => user.name);
console.log(names); // ['Alice', 'Bob', 'Charlie']
3. filter - 数据过滤器
filter 用于筛选数组中满足条件的元素。它接受一个测试函数,返回 true 的元素会被保留到新数组中。
场景:从集合中找出符合条件的子集。
javascript
const numbers = [1, 2, 3, 4, 5, 6];
// 筛选出所有偶数
const evens = numbers.filter(num => num % 2 === 0);
console.log(evens); // [2, 4, 6]
// 筛选出长度大于3的字符串
const words = ['spray', 'limit', 'elite', 'exuberant', 'destruction'];
const longWords = words.filter(word => word.length > 5);
console.log(longWords); // ['exuberant', 'destruction']
4. reduce - 数据聚合器
reduce 是最强大但也较难理解的方法。它将数组中的所有元素通过一个"累加器"函数最终计算为一个单一的值。
arr.reduce(callback(accumulator, currentValue[, index[, array]]), initialValue)
场景:求和、求积、扁平数组、统计频率、对象转换等。
javascript
const numbers = [1, 2, 3, 4];
// 求和
const sum = numbers.reduce((acc, cur) => acc + cur, 0);
console.log(sum); // 10
// 求最大值
const max = numbers.reduce((acc, cur) => (acc > cur ? acc : cur), numbers[0]);
console.log(max); // 4
// 将二维数组扁平化
const flattened = [[0, 1], [2, 3], [4, 5]].reduce(
(acc, cur) => acc.concat(cur),
[]
);
console.log(flattened); // [0, 1, 2, 3, 4, 5]
// 统计数组中每个元素出现的次数
const fruits = ['apple', 'banana', 'apple', 'orange', 'banana', 'apple'];
const count = fruits.reduce((acc, fruit) => {
acc[fruit] = (acc[fruit] || 0) + 1;
return acc;
}, {});
console.log(count); // { apple: 3, banana: 2, orange: 1 }
二、 ES6 新增的搜索方法:find 与 findIndex
在 ES5 中,我们使用 indexOf 和 lastIndexOf 来查找值,但它们对于引用类型或复杂条件查找无能为力。ES6 引入了 find 和 findIndex。
1. find - 查找单个元素
find 方法返回数组中满足提供的测试函数的第一个元素的值。否则返回 undefined。
javascript
const users = [
{ id: 1, name: 'Alice', active: true },
{ id: 2, name: 'Bob', active: false },
{ id: 3, name: 'Charlie', active: true }
];
// 查找第一个 active 为 true 的用户
const activeUser = users.find(user => user.active);
console.log(activeUser); // { id: 1, name: 'Alice', active: true }
// 查找名字为 'Bob' 的用户
const bob = users.find(user => user.name === 'Bob');
console.log(bob); // { id: 2, name: 'Bob', active: false }
// 查找不存在的用户
const nobody = users.find(user => user.name === 'David');
console.log(nobody); // undefined
2. findIndex - 查找元素索引
findIndex 方法返回数组中满足提供的测试函数的第一个元素的索引。否则返回 -1。
javascript
const numbers = [5, 12, 8, 130, 44];
// 查找第一个大于10的元素的索引
const index = numbers.findIndex(num => num > 10);
console.log(index); // 1 (因为 12 > 10,且12的索引是1)
const inactiveIndex = users.findIndex(user => !user.active);
console.log(inactiveIndex); // 1 (Bob的索引)
与 filter 的区别:filter 返回所有匹配的数组,而 find/findIndex 在找到第一个匹配项后立即返回,性能更好。
三、 存在性检查:some 与 every
这两个方法用于逻辑判断,返回布尔值。
1. some - 是否存在一个?
some 测试数组中是不是至少有一个元素通过了提供的函数测试。
javascript
const numbers = [1, 2, 3, 4, 5];
// 数组中有没有偶数?
const hasEven = numbers.some(num => num % 2 === 0);
console.log(hasEven); // true
// 数组中有没有大于10的数?
const hasBigNumber = numbers.some(num => num > 10);
console.log(hasBigNumber); // false
2. every - 是否所有都?
every 测试数组中的所有元素是否都通过了提供的函数测试。
javascript
const numbers = [1, 2, 3, 4, 5];
// 数组中的所有数字都大于0吗?
const allPositive = numbers.every(num => num > 0);
console.log(allPositive); // true
// 数组中的所有数字都是偶数吗?
const allEven = numbers.every(num => num % 2 === 0);
console.log(allEven); // false
四、 数组实例的新方法:includes、flat、flatMap
1. includes - 简单包含判断
includes 方法用来判断一个数组是否包含一个指定的值,如果是返回 true,否则返回 false。它比 indexOf 更语义化,并且能正确判断 NaN。
javascript
const arr = [1, 2, 3, NaN];
// ES5 的 indexOf 问题
console.log(arr.indexOf(2) !== -1); // true
console.log(arr.indexOf(4) !== -1); // false
console.log(arr.indexOf(NaN) !== -1); // false (无法判断NaN)
// ES6 的 includes
console.log(arr.includes(2)); // true
console.log(arr.includes(4)); // false
console.log(arr.includes(NaN)); // true (正确判断)
2. flat - 数组扁平化
flat 方法会按照一个可指定的深度递归遍历数组,并将所有元素与遍历到的子数组中的元素合并为一个新数组返回。
javascript
const arr1 = [1, 2, [3, 4]];
console.log(arr1.flat()); // [1, 2, 3, 4]
const arr2 = [1, 2, [3, 4, [5, 6]]];
console.log(arr2.flat()); // [1, 2, 3, 4, [5, 6]] (默认深度为1)
console.log(arr2.flat(2)); // [1, 2, 3, 4, 5, 6] (指定深度为2)
// 甚至可以用 Infinity 关键字来展开任意深度的嵌套数组
const arr3 = [1, [2, [3, [4, [5]]]]];
console.log(arr3.flat(Infinity)); // [1, 2, 3, 4, 5]
3. flatMap - 先映射后扁平
flatMap 方法首先使用映射函数映射每个元素,然后将结果压缩成一个新数组。它与 map 后紧接着深度为1的 flat 几乎相同,但 flatMap 通常在效率上稍高一些。
javascript
const arr = [1, 2, 3];
// 使用 map 和 flat
const result1 = arr.map(x => [x * 2]).flat();
console.log(result1); // [2, 4, 6]
// 使用 flatMap
const result2 = arr.flatMap(x => [x * 2]);
console.log(result2); // [2, 4, 6]
// 更实用的例子:在处理句子单词时,移除空项
const sentences = ['Hello world', 'My name is Tom', 'Goodbye'];
const words = sentences.flatMap(sentence => sentence.split(' '));
console.log(words);
// ['Hello', 'world', 'My', 'name', 'is', 'Tom', 'Goodbye']
五、 数组的静态方法:Array.from 与 Array.of
1. Array.from - 将类数组对象或可迭代对象转换为数组
类数组对象(拥有 length 属性和若干索引属性的任意对象)和可迭代对象(如 Set, Map, String)可以通过 Array.from 轻松转为真正的数组。
javascript
// 将类数组对象(如 arguments)转换为数组
function foo() {
// const args = Array.prototype.slice.call(arguments); // ES5
const args = Array.from(arguments); // ES6
console.log(args); // [1, 2, 3]
}
foo(1, 2, 3);
// 从 String 创建数组
console.log(Array.from('hello')); // ['h', 'e', 'l', 'l', 'o']
// 从 Set 创建数组(可用于数组去重)
const set = new Set([1, 2, 2, 3, 3, 3]);
const uniqueArray = Array.from(set);
console.log(uniqueArray); // [1, 2, 3]
// 第二个参数:映射函数
const squares = Array.from([1, 2, 3], x => x * x);
console.log(squares); // [1, 4, 9]
2. Array.of - 消除 Array 构造函数的歧义
Array 构造函数在接收不同数量的参数时,行为不一致,这常常令人困惑。Array.of 解决了这个问题。
javascript
// Array 构造函数的问题
console.log(new Array(5)); // [ <5 empty items> ] 一个长度为5的空数组
console.log(new Array(1, 2, 3)); // [1, 2, 3]
// Array.of 的行为始终一致
console.log(Array.of(5)); // [5]
console.log(Array.of(1, 2, 3)); // [1, 2, 3]
六、 方法链与最佳实践
ES6 数组方法的真正威力在于它们可以链式调用,从而以清晰、流畅的方式表达复杂的数据处理逻辑。
场景:有一个用户列表,我们需要找出所有活跃的用户,提取他们的名字,并且这些名字的首字母要大写。
javascript
const users = [
{ id: 1, name: 'alice', active: true },
{ id: 2, name: 'bob', active: false },
{ id: 3, name: 'charlie', active: true },
{ id: 4, name: 'david', active: true }
];
// ES6 方法链
const result = users
.filter(user => user.active) // 筛选活跃用户 -> [{id:1, name:'alice'}, {id:3, name:'charlie'}, {id:4, name:'david'}]
.map(user => user.name) // 提取名字 -> ['alice', 'charlie', 'david']
.map(name => name.charAt(0).toUpperCase() + name.slice(1)) // 首字母大写 -> ['Alice', 'Charlie', 'David']
console.log(result); // ['Alice', 'Charlie', 'David']
这段代码如同一个数据处理的管道,每一步都清晰明了,没有任何中间变量和循环结构,完美体现了声明式编程的魅力。
总结
ES6 的数组方法不仅仅是语法糖,它们代表了一种编程范式的转变。通过拥抱 map、filter、reduce、find、some、every 等方法,我们可以:
-
编写更简洁、更易读的代码:代码更接近于自然语言的描述。
-
减少副作用和错误:避免直接修改原数组和使用易错的循环计数器。
-
提高代码的可维护性和可测试性:每个函数都是纯函数或接近纯函数,易于测试。
-
利用链式调用构建复杂的数据流:让数据处理流程一目了然。