数组方法和可迭代对象 - 提升你的 JavaScript 水平
由 Mux 赞助的 DEV 全球展示挑战赛:展示你的项目!
原文发布于michaelzanggl.com。订阅我的电子报,不错过任何新内容。
今天我想介绍一些数组方法,帮助你提升 JavaScript 水平。
对于所有示例,我们假设有以下变量声明
let users = [
{id: 1, name: 'Michael', active: true, group: 1 },
{id: 2, name: 'Lukas', active: false, group: 2 }
]
本文将向您展示如何实现这一点。
const activeUsernames = []
users.forEach(user => {
if (user.active) {
activeUsernames.push(user.name)
}
})
进入这个
const activeUsernames = users
.filter(user => user.active)
.map(user => user.name)
以及更多。
在代码改进方面,我们希望重点关注以下四个目标。
- 避免使用临时变量
- 避免条件
- 能够分步骤思考你的代码。
- 意图
我们将重点介绍 Array 原型中最重要的一些方法(省略基本的数组操作,如、 或push)pop,希望您能找到一些场景,在这些场景中,您可以应用这些方法而不是以下常用的方法。spliceconcat
for 循环
for (let i = 0; i < users.length; i++) {
//
}
Array.prototype.forEach
users.forEach(function(user) {
//
}
ES6 for 循环
for (const user of users) {
//
}
开始之前还有一件事!
如果您不熟悉 ES6 箭头函数,例如:
users.map(user => user.name)
我建议您先看一下这些。
总而言之,以上内容非常相似,而且在这种情况下,与……相同。
users.map(function(user) {
return user.name
})
数组.原型.过滤器
假设我们想要找到所有活跃用户。我们在文章引言部分已经简要讨论过这个问题。
const activeUsers = []
users.forEach(user => {
if (user.active) {
activeUsers.push(user)
}
})
回顾我们之前设定的四个目标,很明显,这至少违反了其中两个。
它既包含临时变量,也包含条件语句。
我们来看看如何才能让这件事变得更容易。
const activeUsers = users.filter(user => user.active)
其Array.prototype.filter工作原理是:它接受一个函数作为参数(使其成为高阶函数),并返回所有通过测试的用户。在本例中,返回所有活跃用户。
我认为可以肯定地说,我们也已经表明了我们的意图。forEach它可以表示任何意思,例如保存到数据库等等,而它的作用正如filter其名称所示。
当然,你也可以将其应用于filter简单的数组。
以下示例将返回所有以字母 a 开头的动物。
['ape', 'ant', 'giraffe'].filter(animal => animal.startsWith('a'))
我经常看到的另一个用例是从数组中删除元素。假设我们要删除 id 为 1 的用户。我们可以这样做:
users = users.filter(user => user.id !== 1)
过滤器还有以下用途:
const result = [true, 1, 0, false, '', 'hi'].filter(Boolean)
result //? [true, 1, 'hi']
这实际上会从数组中移除所有假值。这里没有什么神奇之处。`is`Boolean是一个接受参数并测试其真假的函数。例如,`is`Boolean('')返回 false,而 ` Boolean('hi')is` 返回 true。我们只需将该函数传递给filter方法,它就充当了我们的测试。
数组.原型.映射
我们经常会遇到这样的情况:有一个数组,我们需要对其中的每个元素进行转换。与其遍历数组,不如直接使用 Map 函数。Map
函数会返回一个长度相同的数组,至于每次迭代返回什么值,则取决于你。
让我们创建一个数组来保存所有用户的用户名。
传统循环
const usernames = []
users.forEach(user => {
usernames.push(user.name)
})
绘制地图
const usernames = users.map(user => user.name)
我们避免使用临时变量,同时揭示意图。
连锁
这些高阶函数的妙处在于它们可以链式调用。map例如,`map` 函数可以遍历数组并返回一个新数组,filter`filter` 函数可以过滤数组并返回一个新数组。看出其中的规律了吗?有了这种特性,编写类似下面的代码不仅可行,而且非常易读。
const activeUsernames = users
.filter(user => user.active)
.map(user => user.name)
至此,我们的最终目标就完成了to think in steps。与其在脑海中完整地思考整个逻辑,不如一步一步地来。想想我们一开始举的例子。
const activeUsernames = []
users.forEach(user => {
if (user.active) {
activeUsernames.push(user.name)
}
})
当你第一次读到这段话时,你脑海中的理解过程大概会是这样的:
- 初始化一个空数组
- 遍历所有用户
- 如果用户处于活动状态
- 从数组开头向数组中添加新元素。
- 但只有用户名
- 从数组开头向数组中添加新元素。
- 如果用户处于活动状态
- 重复
重构后的方法看起来更像这样
- 获取所有活跃用户
- 创建一个大小相同的新数组
- 仅包含他们的用户名
这样更容易思考和推理。
还有许多其他有趣的方法。让我们再来看看。
Array.prototype.find
同样地,filter返回一个包含所有通过测试的项的数组,find返回第一个通过测试的项。
// returns user with id 1
users.find(user => user.id === 1)
Array.prototype.findIndex工作原理相同,但它返回的是索引而不是项目本身。
对于不需要深度检查的数组,无需额外函数带来的开销,只需分别使用 ` get`includes和 ` indexOfget` 即可。
['a', 'b', 'c'].includes('b') //? true
['a', 'b', 'c'].indexOf('a') //? 0
['a', 'b', 'c'].includes('d') //? false
['a', 'b', 'c'].indexOf('d') //? -1
Array.prototype.some
如果至少有一个测试通过,则返回 true。我们可以用它来检查数组中是否至少有一个用户处于活跃状态。
传统解决方案使用 for 循环
let activeUserExists = false
for (let i = 0; i < users.length; i++) {
if (users[i].active) {
activeUserExists = true
break
}
}
使用的解决方案some
users.some(user => user.active)
Array.prototype.every
如果所有项目都通过测试,则返回 true。我们可以使用此方法检查所有用户是否都处于活跃状态。
传统解决方案使用 for 循环
let allUsersAreActive = true
for (let i = 0; i < users.length; i++) {
if (!users[i].active) {
allUsersAreActive = false
break
}
}
使用的解决方案every
users.every(user => user.active)
Array.prototype.reduce
如果以上函数都无法帮到你,那就试试 reduce 函数吧!它本质上就是将数组简化成你想要的任何形式。我们来看一个非常简单的数字示例。我们想要计算数组中所有数字的总和。在传统的 forEach 循环中,代码会是这样的:
const numbers = [5, 4, 1]
let sum = 0
numbers.forEach(number => sum += number)
sum //? 10
但是 reduce 函数可以帮我们省去一些样板代码。
const numbers = [5, 2, 1, 2]
numbers.reduce((result, number) => result + number, 0) //? 10
reduce该函数接受两个参数:一个函数和一个起始值。在本例中,起始值为零。如果我们传递 2 而不是 0,最终结果将为 12。
因此,在下面的例子中
const numbers = [1, 2, 3]
numbers.reduce((result, number) => {
console.log(result, number)
return result + number
}, 0)
日志将显示:
- 0,1
- 1、2
- 3,3
最终结果是最后两个数字 3 和 3 之和,即 6。
当然,我们也可以将对象数组简化为,比如说哈希表。
按键分组后group,生成的 hashMap 应该如下所示
const users = {
1: [
{ id: 1, name: 'Michael' },
],
2: [
{ id: 2, name: 'Lukas' },
],
}
我们可以通过以下代码实现这一点。
users.reduce((result, user) => {
const { group, ...userData } = user
result[group] = result[group] || []
result[group].push(userData)
return result
}, {})
const { group, ...userData } = usergroup从用户处获取密钥,并将剩余的值放入其中userData。result[group] = result[group] || []如果组尚不存在,我们会对其进行初始化。- 我们进入
userData了新群体 - 我们返回下一次迭代的新结果
将这些知识应用于其他可迭代对象和类数组对象
你还记得以前发生过这件事吗?
for 循环:适用于类似数组的对象
for (let i = 0; i < users.length; i++) {
//
}
数组原型上的 Array.prototype.forEach 方法
users.forEach(function(user) {
//
}
ES6 for 循环:适用于可迭代对象
for (const user of users) {
//
}
forEach你有没有意识到这两个循环的语法有多么不同for?
为什么?因为这两个for循环并非只能处理数组。事实上,它们根本不知道数组是什么。
我相信你在计算机科学课上肯定学过这种类型的代码。
const someString = 'Hello World';
for (let i=0; i < someString.length; i++) {
console.log(someString[i]);
}
即使字符串不是数组,我们也可以对其进行遍历。
这种for循环适用于任何“类似数组的对象”,即具有 length 属性和索引元素的对象。
循环for of可以这样使用。
const someString = 'Hello World';
for (const char of someString) {
console.log(char);
}
该for of循环适用于任何可迭代对象。
要检查某个对象是否可迭代,可以使用这行相当简洁的代码Symbol.iterator in Object('pretty much any iterable')。
操作 DOM 时也是如此。如果您现在打开开发者工具并在控制台中执行以下表达式,就会看到一个醒目的红色错误信息。
document.querySelectorAll('div').filter(el => el.classList.contains('text-center'))
遗憾的是,filter可迭代的 DOM 集合中不存在这种方法,因为它们不是数组,因此不共享数组原型中的方法。需要证据吗?
(document.querySelectorAll('div') instanceof Array) //? false
但它是一个类似数组的对象。
> document.querySelectorAll('.contentinfo')
NodeList [div#license.contentinfo]
0: div#license.contentinfo
length: 1
__proto__: NodeList
并且也是可迭代的
Symbol.iterator in Object(document.querySelectorAll('div')) //? true
如果我们想将新学的数组知识应用于可迭代的 DOM 集合,我们首先必须将它们转换为合适的数组。
有两种方法可以做到。
const array = Array.from(document.querySelectorAll('div'))
或者
const array = [...document.querySelectorAll('div')]
我个人更喜欢第一种方法,因为它更易于阅读。
结论
我们学习了数组对象最重要的几个方法,并了解了可迭代对象。回顾我们最初设定的目标,我认为可以肯定地说,我们至少已经实现了这些目标。
- 分步骤思考
- 避免使用临时变量
- 避免条件
但我并不完全满意reveal intent。
尽管
const usernames = users.map(user => user.name)
肯定比……更易读。
const usernames = []
users.forEach(user => {
usernames.push(user.name)
})
不会
const usernames = users.pluck('name')
可以更友善一些吗?
下一篇文章我们将探讨数组的子类化,以便实现类似的功能。这也将是使用 Node.js 进行单元测试的绝佳切入点,敬请期待。
PS:如果您是 Laravel 的粉丝,请看看Laravel Collections。
如果这篇文章对您有帮助,我这里还有很多关于简化写作软件的技巧。
文章来源:https://dev.to/michi/array-methods-and-iterables---stepping-up-your-javascript-game-15bo