Negative array indices thanks to proxies
Published on 22 May 2020
• 4 min
Cette page est également disponible en français.
Even the best things have an end: here comes the last post of our daily series “19 nuggets of vanilla JS.” We wrap up with a bang by looking at a cool use of proxies, that amazing feature of ES2015: allowing negative indices on arrays.
The series of 19
Check out surrounding posts from the series:
- The
for
-of
loop: should there remain only one… - Simulating an abstract class with
new.target
- Negative array indices thanks to proxies (this post)
“Proxy” ?!
I know, I know, cool down. There’s no relation to network proxies. I know how stupidly-configured corporate proxies can be a traumatizing experience, and you have my sympathy.
A proxy is, by definition, an intermediary. ES proxies are exactly that: objects that intercept every possible interaction with another object, and decide on a case-by-case basis whether to let it through, alter it, forbid it…
There is a critical point: a proxy never alters the original object: it is a wrapper of that object, which doesn’t prevent your code from using the original one directly if it holds a reference to it. The idea is that you can pass to external code, when you need it, only the reference to the proxy.
Arrays and negative indices
As a reminder, negative arrays start from the end: -1
is the last element, -2
the one before that, etc. Super handy.
The API for Array
allows negative indices:
slice(from, to)
allows negative values.splice(from, count[, ...items])
allows a negativefrom
.
Unfortunately, the general semantics of the indirect indexing operator, […]
, mandates that the property whose name is evaluated between the square brackets exists with that name. And numerical properties of arrays are not negative.
const fibo = [1, 1, 2, 3, 5, 8, 13]
fibo.slice(-3, -1) // => [5, 8]
fibo.splice(-3, 3) // => [5, 8, 13]
fibo[3] // => 3
fibo[-1] // => undefined 😢
fibo[-1] = 4
fibo // => [1, 1, 2, 3, '-1': 4] 😭
This is sorely needed, wouldn’t you say? Soon we’ll get .at(…)
(on all iterables, too), but still, not as cool!
So let’s add them. 😎
It’s a trap!
A proxy is defined based on two things:
- A target: the original object that we’re about to wrap.
- A handler, that is a plain object featuring predefined methods, called traps. An empty handler will not alter any behavior, making the proxy superfluous.
The language defines one trap per possible interaction with an object. Among others, we have has
intercepting the in
operator for testing the existence of a property, or apply
intercepting, on function objects, the act of calling them (with the (…)
operator).
The general syntax goes like this:
const result = new Proxy(target, {
someTrap(…) { … },
someOtherTrap(…) { … },
})
What we’re interested in are the get
and set
traps, that intercept reading and writing properties. We won’t go as far as ensuring full consistency through extra traps such as has
, ownKeys
and deleteProperty
, because in truth arrays are seldom used in ways other than indexing cells or performing API calls. But if you’d like to go all-out, be my guest!
Implementing read access
OK, let’s start with reading. Here is the general idea:
- We get the name of the requested property (which will technically be either a
String
orSymbol
, as these are the only two valid types for property names in JavaScript). - If that name expresses a negative integer (which we can’t test on a symbol, so we’ll need to be careful), we convert it to its equivalent positive integer by…
- turning it into an actual
Number
- adding it to
length
- turning it into an actual
- As a final step, we delegate to the native implementation of reading a property.
So how do we go about coding this, exactly? A best practice for proxies is to use the Reflect
API, that came along with them and provides a rather low-level access to the native interaction for every trap. So for our get
trap, we would use Reflect.get
, which has the exact same signature.
Let’s get coding:
function makeArrayNegativeFriendly(array) {
return new Proxy(array, {
get(target, prop, receiver) {
if (typeof prop === 'string' && Number(prop) < 0) {
prop = target.length + Number(prop)
}
return Reflect.get(target, prop, receiver)
},
})
}
const fibo = [1, 1, 2, 3, 5, 8, 13]
const niceFibo = makeArrayNegativeFriendly(fibo)
niceFibo[6] // => 13
niceFibo[-1] // => 13 🎉😍
Isn’t life beaaauuuuutiful?
Implementing write access
For writing we’ll go the exact same route, but with the set
trap:
function makeArrayNegativeFriendly(array) {
return new Proxy(array, {
get(target, prop, receiver) {
/* … */
},
set(target, prop, value, receiver) {
if (typeof prop === 'string' && Number(prop) < 0) {
prop = target.length + Number(prop)
}
return Reflect.set(target, prop, value, receiver)
},
})
}
const fibo = [1, 1, 2, 3, 5, 8, 13]
const niceFibo = makeArrayNegativeFriendly(fibo)
niceFibo[-1] = 14
niceFibo[6] // => 14 🎉😍
fibo[6] // => 14 🎉😍
And voilà!
Where can I get that?!
Proxies have been natively supported since Chrome 49, Firefox 18, Opera 36, Edge 12, Safari 10 and Node 6.
However, unlike previous posts in this series, you can’t fallback to transpiling. It is, quite simply, impossible to emulate proxies in ES5. So either it’s native, or you need to hack like crazy with accessors and property descriptors, which is slower, heavier, and most importantly not dynamic at all (properties must be known and wrapped ahead of time, which in our particular example would be either infeasible or extremely cumbersome).
Want to dive deeper (in proxies)?
If this peaked your interest, I explored proxies in-depth (with tons of fun examples and useful ones) in a talk I gave, among other places, at Fronteers 2019 (slides are here).
Our astounding 360° ES training course also dives deep in them.
Want to dive deeper (in general)?
Our trainings are amazeballs, be they in-room or remote online, multi-client or in-house just for your company!
That’s a wrap!
Pfew! There you have it: 19 days, 19 posts on JavaScript “nuggets.” I hope you enjoyed the ride, smiled, had a laugh or two, learnt some things, couldn’t believe some of it, and more. Feel free to tweet about it!
We’ve got more series planned, about new ES2020 stuff and Node.js “nuggets” (Node core and core modules, no third-party code). Keep a sharp eye out!