Qwerty
Qwerty

Reputation: 31919

JavaScript: multiple filters chain vs. complex condition

Very often you need to filter an array with more than one condition:

return results
  .filter(x => controlGroup[x] !== undefined)
  .filter(x => x.size > 10)

or you could do the same with a complex condition and a single filter:

return results
  .filter(x => controlGroup[x] !== undefined && x.size > 10)
// or
  .filter(x => 
    controlGroup[x] !== undefined // <- Prettier would place && here. What a joke. 101
    && x.size > 10
  )

My guess is that since the second approach doesn't iterate the array multiple times, it has better performance characteristics,

on the other hand, the first wins in readability and can be easily extended with neat git diff LOC.


In my particular case I have a function on a Node.js server that runs 3 times per request on a set of ~40 items (after the flat()). There is already lots of iterations and some lambda functions, so I even kind of doubt that micro-optimizing the two filters would make a difference. I believe it could all be rewritten using just a single reduce to save 7 iterations (if I count correctly), but at what cost?
PS: I am looking for a general answer, but you could answer my particular case as a bonus. Thank you.

export const getProductFeatures =
  (technicalSpecifications: TechnicalSpecifications, featuresBySpecLabel: FeaturesBySpecLabel): ProductFeature[] =>
    [...technicalSpecifications
      .map(specGroup => specGroup.specifications)
      .flat()
      .filter(spec => featuresBySpecLabel[spec.label] !== undefined)
      .filter(spec => verifyFeature(spec, featuresBySpecLabel[spec.label]))
      .reduce((productFeatures, spec) => {
        productFeatures.add(featuresBySpecLabel[spec.label].featureLabel)
        return productFeatures
      }, new Set<string>())]
      .map(label => ({ label }))

Upvotes: 2

Views: 2410

Answers (2)

trincot
trincot

Reputation: 350137

Seeing the code at the end of the question, there are some quick wins you can apply:

  • map() followed by flat() can be combined into a flatMap() call.
  • The construct values.reduce((set, value) => set.add(f(value)), new Set) can be accomplished more elegantly as new Set(values.map(f))
  • Intermediate arrays in the chain can be avoided by making use of iterator helper methods, an ECMAScript 2025 novelty.
  • The construct [...set].map(mapper) can be replaced with Array.from(set, mapper) so avoiding an intermediate array.
  • As featuresBySpecLabel[spec.label] is either an object or null, you can omit !== undefined and just rely on the truthiness of featuresBySpecLabel[spec.label]
  • Here and there you could benefit from destructuring a parameter at its declaration.

This results in this update of your code:

const getProductFeatures = (technicalSpecifications, featuresBySpecLabel) =>
    Array.from( // Benefit from mapper parameter to get the final array
        new Set( // Pass iterator to Set constructor
            technicalSpecifications
                .values() // Switch to an iterator chain
                .flatMap(({specifications}) => specifications) // map & flat in one go
                .filter(spec => featuresBySpecLabel[spec.label]) // truthy-check
                .filter(spec => verifyFeature(spec, featuresBySpecLabel[spec.label]))
                .map(({label}) => featuresBySpecLabel[label].featureLabel)
        ),
        label => ({ label })
    );

If you would make sure that verifyFeature will deal well with an undefined second argument -- returning false -- you can omit the first of the two filters.

Upvotes: 1

Qwerty
Qwerty

Reputation: 31919

Based on the comments, there is no significant performance improvement for the particular case of ~40 items in an array.

Technically, each additional iteration increases the time complexity n-times in worst case, so with 2 iterations O(2n), it could take twice as long, but we are talking about time spans way down in the nano-scale, eg. 10ns vs 20ns in this particular case. It might become worth considering depending on the size of the dataset, the machine the code is run on, etc.

Constants are usually ignored when computing O notation. I.e. O(n) vs O(2n).
~ Felix Kling (taken from comments)

That being said, the performance can be taken out of the question, which now leaves only the readability factor, which is hugely subjective and opinion based. Such QAs are not allowed on SO. To get an answer to this, anyone wondering should probably discuss this with their team or decide by heart. Generally though, readability is more often than not valued more for an equivalent code, which the above is. However, what anyone considers better readable depends on personal preferences or team/company guidelines.

Upvotes: 6

Related Questions