A very early draft was published last week by the ECMAScript editor Brian Terlson. When I say very early I mean it’s considered a “stage -1” proposal, meaning it’s not even a formal proposal yet, just a very early draft.
That being said, I’m always excited about new Array.prototype
methods so I decided to write an article nonetheless. These kinds of methods were popularized in JavaScript by libraries like Underscore and then Lodash – and some of them – such as .includes
, have eventually started finding their way into the language.
Shall we take a look?
Array.prototype.flatten
The .flatten
proposal will take an array and return a new array where the old array was flattened recursively. The following bits of code represent the Array.prototype.flatten
API.
[1, 2, 3, 4].flatten() // <- [1, 2, 3, 4]
[1, [2, 3], 4].flatten() // <- [1, 2, 3, 4]
[1, [2, [3]], 4].flatten() // <- [1, 2, 3, 4]
One could implement a polyfill for .flatten
thus far like below. I separated the implementation of flatten
from the polyfill so that you don’t necessarily have to use it as a polyfill if you just want to use the method without changing Array.prototype
.
Array.prototype.flatten = function () {
return flatten(this)
}
function flatten (list) {
return list.reduce((a, b) => (Array.isArray(b) ? a.push(...flatten(b)) : a.push(b), a), [])
}
Keep in mind that the code above might not be the most efficient approach to array flattening, but it accomplishes recursive array flattening in a few lines of code. Here’s how it works.
- A consumer calls
x.flatten()
- The
x
list is reduced using.reduce
into a new array[]
nameda
- Each item
b
inx
is evaluated throughArray.isArray
- Items that aren’t an array are pushed to
a
- Items that are an array are flattened into a new array
- Those items are spread over a
.push
call fora
- Those items are spread over a
- This eventually results in a flat array
The proposal also comes with an optional depth
parameter – that defaults to Infinity
– which can be used to determine how deep the flattening should go.
[1, [2, [3]], 4].flatten() // <- [1, 2, 3, 4]
[1, [2, [3]], 4].flatten(2) // <- [1, 2, 3, 4]
[1, [2, [3]], 4].flatten(1) // <- [1, 2, [3], 4]
[1, [2, [3]], 4].flatten(0) // <- [1, [2, [3]], 4]
Adding the depth
option to our polyfill wouldn’t be that hard, we pass it down to recursive flatten
calls and ensure that, when the bottom is reached, we stop flattening and recursion.
Array.prototype.flatten = function (depth=Infinity) {
return flatten(this, depth)
}
function flatten (list, depth) {
if (depth === 0) {
return list
}
return list.reduce((accumulator, item) => {
if (Array.isArray(item)) {
accumulator.push(...flatten(item, depth - 1))
} else {
accumulator.push(item)
}
return accumulator
}, [])
}
Alternatively – for Internet points – we could fit the whole of flatten
in a single expression.
function flatten (list, depth) {
return depth === 0 ? list : list.reduce((a, b) => (Array.isArray(b) ?
a.push(...flatten(b, depth - 1)) :
a.push(b), a), [])
}
Then there’s .flatMap
.
Array.prototype.flatMap
This method is convenient because of how often use cases come up where it might be appropriate, and at the same time it provides a small boost in performance, as we’ll note next.
Taking into account the polyfill we created earlier for flattening through Array.prototype.flatten
, the .flatMap
method can be represented in code like below. Note how you can provide a mapping function fn
and its ctx
context as usual, but the flattening is fixed at a depth of 1
.
Array.prototype.flatMap = function (fn, ctx) {
return this.map(fn, ctx).flatten(1)
}
Typically, the code shown above is how you would implement .flatMap
in user code, but the native .flatMap
trades a bit of readability for performance, by introducing the ability to map items directly in the internal flatten procedure, avoiding the two-pass that’s necessary if we first .map
and then .flatten
an Array
.
A possible example of using .flatMap
can be found below.
[{ x: 1, y: 2 }, { x: 3, y: 4 }, { x: 5, y: 6 }].flatMap(c => [c.x, c.y])
// <- [1, 2, 3, 4, 5, 6]
The above is syntactic sugar for doing .map(c => [c.x, c.y]).flatten()
while providing a small performance boost by avoiding the aforementioned two-pass when first mapping and then flattening.
Note that our previous polyfill doesn’t cover the performance boost, let’s fix that by changing our own internal flatten
function and adjust Array.prototype.flatMap
accordingly. We’ve added a couple more parameters to flatten
, where we allow the item to be mapped into a different value right before flattening, and avoiding the extra loop over the array.
Array.prototype.flatMap = function (fn, ctx) {
return flatten(this, 1, fn, ctx)
}
function flatten (list, depth, mapperFn, mapperCtx) {
if (depth === 0) {
return list
}
return list.reduce((accumulator, item, i) => {
if (mapperFn) {
item = mapperFn.call(mapperCtx || list, item, i, list)
}
if (Array.isArray(item)) {
accumulator.push(...flatten(item, depth - 1))
} else {
accumulator.push(item)
}
return accumulator
}, [])
}
Since the
mapperFn
andmapperCtx
parameters offlatten
are entirely optional, we could still use this same internalflatten
function to polyfill both.flatten
and.flatMap
.
Comments