by Balaganesh Damodaran
通过Balaganesh Damodaran
JavaScript’s Array class exposes quite a few methods (filter, map, reduce), which iterate through an array and call an iterator function to perform actions on the array. Chaining these methods allows you to write clean, easy to read code. But what does this convenience cost us in terms of performance, and is it worth it?
JavaScript的Array类公开了很多方法(过滤,映射,归约),这些方法遍历数组并调用迭代器函数以对该数组执行操作。 链接这些方法使您可以编写干净,易于阅读的代码。 但是就性能而言,这种便利给我们带来了什么代价,值得吗?
Javascript is a ‘functional’ language. What this means is that functions are first class objects in Javascript, and as such they can be passed on as parameters to other functions. There are quite a few built-in methods provided by the Javascript standard library, which makes use of this fact to enable us to write clean, understandable, and easy to read code.
Javascript是一种“功能性”语言。 这意味着函数是Javascript中的第一类对象,因此它们可以作为参数传递给其他函数。 Javascript标准库提供了许多内置方法,利用此事实,我们可以编写干净,可理解且易于阅读的代码。
内置Javascript数组方法和链接 (Built-in Javascript Array Methods, and Chaining)
One such built-in class which makes extensive use of Javascript’s functional nature is the Array
class. Array
s in Javascript expose a number of instance methods, which:
Array
类是其中一种广泛使用Javascript功能性质的内置类。 Javascript中的Array
公开了许多实例方法,这些方法:
- accept a function as an argument, 接受一个函数作为参数,
- iterate upon the array, 遍历数组,
- and call the function, passing along the array item as a parameter to the function. 并调用函数,将数组项作为参数传递给函数。
The most popular of these are of course forEach
, filter
, map
and reduce
. Since some of these methods also return the Array
instance as the return value of the method, they are often chained together like this:
其中最受欢迎的当然是forEach
, filter
, map
和reduce
。 由于其中一些方法还将Array
实例作为方法的返回值返回,因此它们通常像这样链接在一起:
const tripExpenses = [{ amount: 12.07, currency: 'USD', paid: true}, { amount: 1.12, currency: 'USD', paid: true}, { amount: 112.00, currency: 'INR', paid: false}, { amount: 54.17, currency: 'USD', paid: true}, { amount: 16.50, currency: 'USD', paid: true}, { amount: 189.50, currency: 'INR', paid: false}];
const totalPaidExpensesInINR = tripExpenses .filter(expense => expense.paid) .map(expense => { if(expense.currency == 'USD') return expense.amount * 70; else return expense.amount; }) .reduce((amountA, amountB) => amountA + amountB);
In this example, we are calculating the total paid expenses after converting them from USD to INR. To do this, we are:
在此示例中,我们在将总支出从美元转换为印度卢比后计算出总支出。 为此,我们是:
filter
ingtripExpenses
to extract only the paid expenses,filter
tripExpenses
费用以仅提取已支付的费用,map
ping the expense amount from the specified currency and converting it to INR, and从指定货币
map
ping费用金额并将其转换为INR,以及reduce
ing the INR amounts to get the sum.reduce
INR金额即可获得总和。
Looks like a common, very typical, valid use-case for chaining array methods right? A lot of developers who’ve been taught to write functional Javascript would come out with something similar when asked to solve this issue.
看起来像是链接数组方法的常见,非常典型的有效用例,对吗? 当被要求解决这个问题时,许多被教导编写功能性Javascript的开发人员都会有类似的想法。
数组方法链接的问题 (The Problem with Array Method Chaining)
Currently, our tripExpenses
array only has 6 items, so this is relatively fast. But what happens when we have to analyze the trip expenses for, say, an entire company of employees for the entire financial year, and our tripExpenses
array starts to have hundreds of thousands of elements?
当前,我们的tripExpenses
数组只有6个项目,因此相对较快。 但是,当我们不得不分析整个财务年度中整个员工公司的旅行费用,而我们的tripExpenses
数组开始包含成千上万个元素时,会发生什么呢?
Thanks to JSPerf, we can visualize this cost quite easily. So let’s run a comparison test for the same code with tripExpenses
having 10 elements, 10,000 elements, and 100,000 elements. Here's the result of the JSPerf comparison:
感谢JSPerf,我们可以很容易地可视化此成本。 因此,让我们对具有10个元素,10,000个元素和100,000个元素的tripExpenses
进行相同代码的比较测试。 这是JSPerf比较的结果:
The graph shows the number of operations per second, and higher is better. While I expected the 100,000 elements case to perform poorly, I really did not expect the 10,000 elements case to perform this poorly. Since it’s not really visible on the chart, let’s look at the numbers:
该图显示每秒的操作数,越高越好。 虽然我希望100,000个元素的案例效果不佳,但我真的没想到10,000个元素的案例效果不佳。 由于它在图表上并不真正可见,因此让我们看一下数字:
- 10 Elements — 6,142,739 ops per second 10个元素-每秒6,142,739个操作
- 10,000 Elements — 2,199 ops per second 10,000个元素-每秒2,199个操作
- 100,000 Elements — 223 ops per second 100,000个元素—每秒223次操作
Yikes, that’s really bad! And while processing an array of 100,000 elements might not happen often, 10,000 elements is a very plausible use case that I’ve seen regularly in multiple applications I’ve developed (mostly on the server side).
kes,那真是太糟糕了! 尽管处理10万个元素的数组可能不会经常发生,但是10,000个元素是一个非常合理的用例,我已经在自己开发的多个应用程序中定期看到(大多数是在服务器端)。
This shows us that when we write — even what seems to be quite simple code — we really should be watching out for any performance issues that might crop up because of the way we write our code.
这向我们表明,当我们编写甚至是看起来很简单的代码时,我们确实应该提防由于编写代码的方式而可能出现的任何性能问题。
If, instead of chaining the filter
, map
and reduce
methods together, we rewrite our code such that all the work gets done inline, in a single loop, we can gain significantly better performance.
如果不是将filter
, map
和reduce
方法链接在一起,而是重写代码,使所有工作都可以在一个循环中内联完成,则可以显着提高性能。
let totalPaidExpensesInINR = 0;
for(let expense of tripExpenses){ if(expense.paid){ if(expense.currency == 'USD') totalPaidExpensesInINR += (expense.amount * 70); else totalPaidExpensesInINR += expense.amount; }}
Let’s run another JSPerf comparison to see how this performs against it’s functional counterpart, in a 10,000 element test:
让我们运行另一个JSPerf比较 ,以在10,000个元素的测试中查看其相对于其功能副本的性能:
As you can see, on Chrome (and by extension Node.JS), the functional example is a whopping 77% slower than the for-of example. On Firefox, the numbers are a lot closer, but the functional example is still 16% slower than the for-of example.
如您所见,在Chrome(以及扩展名为Node.JS)上,该功能示例比上述示例慢了77%。 在Firefox上,数字更接近,但功能示例仍比上述示例慢16%。
为什么会有这么大的性能增量? (Why Such a Large Performance Delta?)
So why is the functional example so much slower than the for-of example? Well it’s a combination of factors, but the primary factors that, as a developer, we can control from user land are:
那么,为什么功能示例比常规示例要慢得多? 好吧,这是多种因素的综合,但是作为开发人员,我们可以从用户领域控制的主要因素是:
- Looping over the same array elements multiple times. 循环遍历同一数组元素多次。
- Overhead of function calls for each iteration in the functional example. 在函数示例中,函数开销要求每次迭代。
If you see the for-of example, you’ll see that we only ever iterate through the tripExpenses
array once. Also, we call no functions from within, instead performing our calculations inline.
如果看到上述示例,您将看到我们仅对tripExpenses
数组进行一次迭代。 另外,我们从内部不调用任何函数,而是内联执行计算。
One of the big performance ‘wins’ that modern Javascript engines get is by inlining function calls. What this means is that the engine will actually compile your code into a version where the compiler replaces the function call, with the function itself (i.e. inline where you call the function). This eliminates the overhead of calling the function, and provides huge performance gains.
现代Javascript引擎获得的最大性能“胜利”之一是通过内联函数调用。 这意味着引擎实际上会将您的代码编译为一个版本,在该版本中,编译器将函数本身替换为函数调用(即,内联调用函数)。 这消除了调用函数的开销,并提供了巨大的性能提升。
However, we cannot always say for sure whether a Javascript engine will choose to inline a function or not, so doing it ourselves ensures that we have the best possible performance.
但是,我们不能总是肯定地说Java引擎是否会选择内联函数,因此,我们自己执行此操作可确保我们具有最佳的性能。
结论 (Conclusion)
Some developers may consider the for-of example to be less readable, and more difficult to understand than the functional example. For this particular example, I’d say both the styles are equally readable. However, in the case of the functional example, the convenience of the method chaining tends to hide the multiple iterations and function calls from the developer, thus making it easy for an inexperienced developer to write non-performant code.
一些开发人员可能认为for-of例子比功能性例子可读性差,并且更难以理解。 对于这个特定的示例,我想说两种样式都具有相同的可读性。 但是,在功能示例的情况下,方法链接的便利性倾向于隐藏来自开发人员的多次迭代和函数调用,从而使经验不足的开发人员更容易编写性能不佳的代码。
I’m not saying that you should always avoid the functional way – I’m sure there are plenty of valid cases for using the functional way and for chaining the methods. But a general rule of thumb to remember when it comes to performance and iterating arrays in Javascript is that if you’re chaining methods which iterate through the entire array, you should probably stop and consider the performance impact before going ahead.
我并不是说您应该始终避免使用功能性方法–我敢肯定,有很多有效的案例可以使用功能性方法并链接方法。 但是要记住有关在Java中对性能和迭代数组进行操作的一般经验法则是,如果要链接遍历整个数组的方法,则可能应该停下来并考虑性能影响,然后再继续。
I’d love to hear your opinion on what I’ve written in this article. Do chime in with your comments below.
我很想听听您对我在本文中所写内容的看法。 请在下面提出您的意见。
[2019年2月6日]评论员指出的一些警告和注意事项 ([Feb 6th, 2019] Some caveats, and things to keep in mind, as pointed out by commenters)
As pointed out by Paul B, there are is a performance hit when using `for…of` in a transpiled form in browsers, but you can always use a normal for loop with an iterator variable to get around this. However, like Paul says, there are quite a few advantages to sticking with an iterator function. Do go read his comment, it’s worthy of being an article all by itself.
正如Paul B所指出的,在浏览器中以转译形式使用`for…of`时,性能会受到影响,但是您始终可以使用带有迭代器变量的普通for循环来解决此问题。 但是,就像Paul所说的那样,坚持使用迭代器功能有很多优点。 请务必阅读他的评论,值得单独发表一篇文章。
In addition, a lot of folks have also been saying that this would be premature optimization or a micro optimization, and I do partially agree with them. You should in general always optimize for readability and maintainability over performance, right up until the point where poor performance actually starts impacting you. Once you’ve reached that point, then you might want to reconsider your iterators.
另外,许多人也一直在说这是过早的优化还是微观的优化,我确实部分同意他们的观点。 通常,您应该始终针对性能的可读性和可维护性进行优化,直到性能下降开始对您造成影响为止。 达到这一点后,您可能需要重新考虑迭代器。
Originally published at asleepysamurai.com on January 8, 2019.
最初于2019年1月8日发布在asleepysamurai.com上。
翻译自: https://www.freecodecamp.org/news/beware-of-chaining-array-methods-in-javascript-ef3983b60fbc/