在Web前端开发中,如何优化前端数组处理方法?也许你已经遇到过讨论,我们是否应该使用Array
方法(filter
,map
,reduce
等)或循环(for
,for...of
,等)遍历集合。
关于数组的一些方法,我们分享过文章:
大家有兴趣的,可以去一一了解下。一段好的代码,会让你的程序性能更好且易理解,运行速度加快很多,相信大家都会知道这一点。
性能和可读性是选择一个而不是另一个的两个常见论点。让我们更好地看一下循环和Array
方法之间的差异并进行比较,以便我们自己决定。
性能
在ECMAScript 5之前,我们所拥有的只是循环和jQuery $.each
。哪种迭代最快的方法经常引起争议。从那时起,情况有了很大的改善。从那时起,我们智能手机中的CPU的性能就超过了计算机,我们现在可以在客户端做更多很棒的工作,而不会崩溃整个浏览器。尽管如此,Array
仍然提高了方法的性能。
TIPs: 如果要遍历大量项目,则可能要避免使用
Array
方法。
在大多数情况下,它不应有太大的区别。对于大多数项目,很少要遍历一百多个项目的列表。您通常可以使用您认为更易读的内容。只要确保始终检查低端设备的性能即可。
可读性
我们可以同意可读性很重要。这也是主观的。例如,让我们使用带有隐式return((a, b) => a + b
)的箭头函数。初学者可能会将其与逻辑表达式混淆。大于或等于运算符(>=
)看起来确实相似。也许他们可能没有意识到函数返回值。毕竟,返回是隐式的。训练有素的眼睛会立即注意到箭头的功能。对于所有其他语法也可以这样说。由我们决定培训初学者开发人员或不使用功能,以便任何技能水平的开发人员都可以访问代码库。话虽如此,让我们比较一些循环和Array
方法的可读性。
Array.prototype.forEach而且for...of都易于阅读。
posts.forEach((post) => console.log(post));
// or
for (const post of posts) {
console.log(post);
}
等效的循环Array.prototype.filter
有点显式。
const drafts = posts.filter((post) => post.isDraft);
// or
const drafts = [];
for (const post of posts) {
if (post.isDraft) {
drafts.push(post);
}
}
也是一样Array.prototype.map
。
const publishDates = post.map((post) => post.date);
// or
const publishDates = new Array(posts.length);
for (let i = 0; i < posts.length; i++) {
publishDates[i] = posts[i].date;
}
Array.prototype.reduce
确实很棒,但是可能要花几秒钟来了解正在发生的事情。即使对于类似求和的运算,您可能也需要一秒钟才能理解。当reduce回调包含更多逻辑时,可读性迅速变差。
const totalWordCount = posts
.reduce((result, post) => result + post.wordCount, 0);
// or
let totalWordCount = 0;
for (const post of posts) {
totalWordCount += post.wordCount;
}
对于其他方法,例如Array.prototype.every
,,Array.prototype.find
和Array.prototype.some
,循环并不可怕,并且在具有早期返回功能的函数中效果更好。它们绝对比Array
方法更明确。
const isEverythingPublished = posts.every((post) => post.isDraft);
// or
let isEverythingPublished = true;
for (const post of posts) {
if (post.isDraft) {
isEverythingPublished = false;
break;
}
}
// or in a function with early return
function getIsEverythingPublished(posts) {
for (const post of posts) {
if (post.isDraft) {
return false;
}
}
return true;
}
const isEverythingPublished = getIsEverythingPublished(posts);
如果您喜欢明确的话,循环可能会吸引您。对我来说,它们有点冗长,我个人更喜欢Array
除之外的方法Array.prototype.reduce
。但同样,可读性是主观的。这是私事。
特点与用途
尽管看起来循环和Array
方法是做同一件事的两种方式,但是它们在显着方式上是不同的。
首先,Array
方法是同步的。如果将异步函数传递给Array
方法,它将不会等待异步函数完成。循环不依赖于回调,因此,如果您已经处于异步上下文中,则可以await
在循环块中或使用for await...of
循环。
考虑下面的示例。这是一劳永逸的操作,传递给异步函数的Array.prototype.forEach
调用被调用,但forEach
不等待该函数完成。
const urls = [
'https://www.jiangweishan.com',
'https://www.web176.com',
];
urls.forEach(async () => {
const response = await fetch(url);
if (!response.ok) {
console.warn(`Unable to fetch ${url}`);
}
});
在下面的示例中,我们将异步函数传递给Array.prototype.map
调用。因为一个异步函数总是返回Promise
时,返回值map
调用的列表Promise
。我们可传递给Promise.all
,Promise.allSettled
或Promise.race
。这样,我们可以并行运行各种异步任务并等待结果。
async function assertPageIsOk(url) {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`"GET ${url}" responded with ${response.statusCode}`);
}
return true;
}
const urls = [
'https://timseverien.com',
'https://timseverien.com/posts',
];
try {
await Promise.all(urls.map(assertPageIsOk));
} catch (error) {
console.error(error);
}
让我们使前面的示例稍微复杂一点。让我们依次调用每个URL,而不是同时触发所有请求(可能导致拒绝服务)。如果其中之一返回非2xx状态代码,我们可以停止序列。
function assertPagesAreOk(urls) {
return urls.reduce(async (chain, url) => {
await chain;
return assertPageIsOk(url);
}, Promise.resolve());
}
assertPagesAreOk(urls)
.then(() => ...)
.catch(() => ...);
这是上面的循环等效项:
async function assertPagesAreOk(urls) {
for (const url of urls) {
await assertPageIsOk(url);
}
return true;
}
assertPagesAreOk(urls)
.then(() => ...)
.catch(() => ...);
在以上所有示例中,我发现循环均等或更具可读性。
循环和Array
方法之间的第二个重要区别是数组不能无限大。通常,我们认为无限循环是一件坏事,但它们可能非常有用。
想象一下,我们正在开发一个现代散点图库,该库中我们想在画布上绘制大量点。由于数组大小有限,因此我们可以通过允许用户传递生成器函数来利用迭代器。
import createScatterPlot from 'scatman';
createScatterPlot({
data: *function() {
while (true) {
yield {
x: Math.random(),
y: Math.random(),
};
}
},
});
生成器函数返回生成器对象,它是迭代器的超集。它们可以转换为数组(通过[...iterator]
或Array.from(iterator)
),也可以循环使用。如前所述,数组的最大大小是一个限制。另一方面,在循环中,当循环需要下一个值时,会将各个值从迭代器中拉出。因为我们在示例中生成了无限点,所以将其转换为数组将冻结浏览器。
for (const { x, y } of data) {
draw(x, y);
await waitForNextFrame();
}
总结
我们已经知道循环速度更快,但是Array
当方法内部处理某些逻辑(例如创建新数组或滤除特定值)时,方法可以提高可读性。我们还看到,在处理异步任务时,循环更具可读性,并了解到循环对于迭代未知数量的项目很有用,这在迭代器中很常见。
在大多数情况下,我们将使用它们中的任何一个来遍历少于一千个项目的数组,在这种情况下,我们应该强烈倾向于更具可读性的选项。我觉得Array
除以外的方法更具可读性Array.prototype.reduce
。我经常看到前者在使用这种方法时遇到了麻烦,而没有完全了解它的行为。
无论如何,没有人比您更了解您,您的团队或您的项目。也许您是在一个全栈的团队中工作的,他们对循环比对Array
方法更熟悉。也许这些Array
方法更符合您的函数式编程原理。您(和您的团队)将不得不弄清楚这一点。