array slinging in JavaScript
This is one part of an ongoing style guide for modern front-end JavaScript development. Stay tuned for more: @mattdesl.
Functional programming may be a nebulous concept to some front-end developers, but even learning the basics can greatly improve the readability and flexibility of your code. Here we’ll take a brief look at filter
, map
, etc and how they can lead to cleaner and more “functional” code.
Briefly explained:
filter
creates a new array with only the elements that pass the test of our given functionmap
creates a new array where the elements are the result of calling our given function
Full details in the docs. You should also get comfortable with reduce
, reduceRight
, some
and every
.
For example, let’s say we have a list of person objects and we are trying to filter them by age:
var persons = [
{ name: "Will", age: 15 },
{ name: "John", age: 49 },
{ name: "Samantha", age: 24 },
{ name: "Ruth", age: 50 }
]
The typical “imperative” approach might look like this:
function getAdultNames(persons) {
var adults = []
for (var i=0; i<persons.length; i++) {
var p = persons[i]
if (p.age > 25)
adults.push(p.name)
}
return adults
}
var names = getAdultNames(persons)
Instead, we could rewrite it with Array filter
and map
. For most of these native Array methods, the function we give will be called with the parameters value
(the element being operated on), index
(its index in the array), array
(the array being traversed). In our case we only need the first parameter.
var names = persons.filter(function(p) {
return p.age > 25
}).map(function(p) {
return p.name
})
It leads to clear and concise code with less room for typos (like forgetting to write .length
in the for declaration).
But, the real benefit becomes evident as the program begins to grow in complexity. Now let’s say we need another function to filter only teens. We could write a getTeenNames
function with its own loop (code duplication), or maybe we would add the new logic in the same loop (no separation of concerns), or some other ugly thing. Or, with the functional approach, we could write it like so:
function getNames(persons, threshold) {
return persons.filter(threshold).map(function(p) {
return p.name
})
}
function adult(p) {
return p.age > 25
}
function teen(p) {
return p.age >= 13 && p.age <= 19
}
var adults = getNames(persons, adult)
var teens = getNames(persons, teen)
When you start creating larger applications built of many pieces, this sort of functional design goes a long way. For example, here is a concise module that uses some()
to determine whether an array of pixels (i.e. an image) is grayscale.
var rgb2hsv = require('color-convert').rgb2hsv
function hasSaturation(hsv) {
return hsv[1] > 0
}
//expects rgb array: [ [255, 0, 255], [128, 18, 18], .. ]
module.exports = function(colors) {
return !colors.map(rgb2hsv).some(hasSaturation)
}
Then, assuming we have a list of images like this:
//our list of "images"
var imgA = [ [255,0,255], [155,100,0] ]
var imgB = [ [128,128,128], [100,100,100] ]
var images = [imgA, imgB]
We can filter them like so:
var isGrayscale = require('is-grayscale')
//filter to a list of grayscale images
var grays = images.filter(isGrayscale)
Generally speaking, when you find yourself writing a for
loop, you should consider whether one of the native methods would be better suited. This pattern will become more prevalent when ES6 brings arrow functions, array comprehension, and a few other goodies. For example, in ES6 you could write the following to get the names of the 3 oldest teens, sorted by oldest to youngest:
var oldest = persons.slice()
.sort((a, b) => b.age - a.age)
.filter(teen)
.slice(0, 3)
.map(p => p.name)
the first slice()
is so we don’t destructively sort persons
A note on performance and memory: it is almost always negligible unless you are operating on many hundreds of thousands of entries per frame (which may be the case with our above grayscale check). As always, benchmark first to avoid prematurely optimizing.