JS protip: Grabbing last items from an array
By Christophe Porteneuve • Published on 19 October 2022
• 3 min
Cette page est également disponible en français.
We quite often need to grab one or more items from the tail end of an array or string. Historically it’s been quite a pain in the neck, but things have changed!
Old school…
Let’s start with time-honored recipes. Although on this one, we’re likely better off forsaking tradition and embracing newer ways!
You may be aware that the slice()
method (on Array
and String
) and splice()
method (on Array
) allow negative indices: they behave just as if they were added to length
, so that −1 refers to the last item, −2 to the next-to-last one, and so forth:
// 😐 Meh. Negative indices alright, but through `slice()`.
const first = 'Maxence'
const wonders = [first, 'Louise', 'Anna', 'Elliott']
wonders.slice(-1) // => ['Elliott']
wonders.slice(-2) // => ['Anna', 'Elliott']
wonders.slice(-2, -1) // => ['Anna']
first.slice(-1) // => 'e'
Alas! Not only does Array#slice()
return an array (when we usually want the item itself), forcing us to blurt out an ugly items.slice(-1)[0]
, but negative indices wouldn’t work anyway with the indirect indexing operator []
, which is our go-to approach when grabbing an item from an array (or perhaps a string):
// ❌ Er, nope.
const first = 'Maxence'
const wonders = [first, 'Louise', 'Anna', 'Elliott']
wonders[-1] // => undefined
first[-1] // => undefined
The reason behind this is simple: the semantics of []
have nothing to do with positions. This operator accepts an expression resolving to a property name, and either the property exists under that name (we then get its value) or there’s no such name for a property (we then get undefined
).
This is why we always end up with that train wreck:
// 😭 So verboooooooooose
const first = 'Maxence'
const wonders = [first, 'Louise', 'Anna', 'Elliott']
wonders[wonders.length - 1] // => 'Elliott'
first[first.length - 1] // => 'e'
Thanks, but no thanks.
Negative indices by wrapping with a proxy?
ES2015 gave us ES proxies (check out my talk at Fronteers 2019 or the MDN docs).
If we can access an object wrapped with the relevant proxy, we suddenly gain the ability to perform negative indexing! As to whether you’d like doing that whenever you return an array, to allow that kind of indexing for your calling code, well, that’s up to you.
// 🤯 A cool little ES proxy…
function allowNegativeIndices(obj) {
return new Proxy(obj, {
get(target, prop, receiver) {
return Reflect.get(target, checkIndexAccess(target, prop), receiver)
},
set(target, prop, receiver) {
return Reflect.set(target, checkIndexAccess(target, prop), receiver)
},
})
}
function checkIndexAccess(target, prop) {
return typeof prop !== 'symbol' && prop < 0
? target.length + Number(prop)
: prop
}
Let’s see what that feels like:
// 😎 Negative indices represent!
const wonders = allowNegativeIndices(['Maxence', 'Louise', 'Anna', 'Elliott'])
wonders[-1] // => 'Elliott'
at()
on built-in iterables
OK, but we can’t always afford (or don’t want) to wrap our arrays with bespoke proxies. So what’s a developer to do?
Well, ES2022 brought us the at(index)
method on all built-in position-based iterables: Array
, types arrays and String
.
// 😎 I READ THE DOCS! 🦸🏻
const first = 'Maxence'
const wonders = [first, 'Louise', 'Anna', 'Elliott']
first.at(-1) // => 'e'
wonders.at(-2) // => 'Anna'
findLast()
and findLastIndex()
ES2023 will introduce two new helper methods on Array
and typed arrays, that will simplify searching for stuff from the tail end of an array, as opposed to going from the start.
After all, we’ve had lastIndexOf()
and reduceRight()
since ES5, but ES2015 failed to provide “from the tail end” variants of its find()
and findIndex()
novelties. It was ample time we finally got around to this, as we’d been left stranded in manual-numeric-loop hell or having to first do a costly (and mutative!) reverse()
ahead of search!
// 😎 I keep track of what's up with ECMAScript...
const wonders = ['Maxence', 'Louise', 'Anna', 'Elliott']
wonders.findLast((name) => name.match(/[^aeiouy]/i)) // => 'Louise'
wonders.findLastIndex((name) => name.length < 7) // => 2
findLast()
and findLastIndex()
won’t be official until June 2023, but they’re already natively supported everywhere (Safari 15.4, Firefox 104, Chrome/Edge 97, Node 18, Deno 1.24), and for other runtimes, you can easily polyfill it with core-js (via Babel / TS or not).
Protips galore!
We got tons of articles, with a lot more to come. Also check out our kick-ass training courses 🔥!