Python-like list/array operator in JavaScript
By: Kacper Kula
Tags: JavaScript, Numpy, Python

When switching between different languages I often find myself writing the code which would work in one technology but is not valid syntax in another. It happens quite often when I try to access the last element of an array in JavaScript. I often try to refer to -1 index, which is perfectly fine in Python, but does not really work in JS. Because of how arrays are implemented in JavaScript, it doesn’t even warn you when you try to access it. In fact, the following is completely legal in JS:

Because internally JavaScript array behaves like a map, it doesn’t care if the index isn’t what it expected — it will just create a map entry for it. Sometimes it might be useful, but mostly it is a sign of an error in your code.

It made me think — would it be possible to hack array [] operator to make it behave similarly to the one you can find in Python (or even go step further and implement some of numpy arrays functionality). It turns out it is possible using the power of Proxies!

Getter accepting negative indices

Let’s start with allowing JavaScript to get elements providing negative indices. We can do it easily by using Proxy class. It is a JavaScript mechanism for meta-programming — changing behaviour of a language rather than using it on an application level. We can wrap any JavaScript object (and even functions) with a proxy and intercept it’s method calls.

The second native object we are using is Reflect which is a simple facade for getters and setters for different kinds of objects. Thanks to it we can use the same code with different types of objects (think of common API for both Array and ES6 Map).

Proxy takes two arguments — target object which it extends and a map of functions that it overrides. In this example, we only override getter by providing a custom function. Getter takes three arguments:

target which is an original array

which is an original array prop which is a property we are accessing (converted to a string)

which is a property we are accessing (converted to a string) reciever which is an instance of the Proxy class

Let’s analyse the code: if the property that was provided is smaller than 0, we compute the proper index by adding our value to the length of an array. If it was positive, we just use getter without changing the parameters. (using Reflect.get which in this case is equivalent to calling target[prop] directly).

Accepting ranges

In numPy it is quite common to access multiple elements at once by providing an array of indices. It would be cool if JavaScript would support that, wouldn’t it? We can easily extend our solution to do it for us too.

Because JavaScript automatically converts all the properties to strings, an array [1, -1] will become a string 1,-1 . Using split we can then reverse-engineer if the argument was initially an array and if so, we can call the getter for each of the elements individually.

Setter

I a similar way as we wrote our getter function, we can extend the setter. This will allow us to set the last element by simply calling l[-1] = 5 . The only difference is that set function takes an additional parameter which is the value we are trying to set.

Setter for ranges

We can go even step further and accept an array of indices — the same way as you can do it numPy. We can then set value for multiple indices at once: l[[1, -1]] = 5 . Notice that we can combine our solutions — we can provide negative indices in the ranges too.

Similarly to what we did in a getter, we check if our prop contains a semicolon and, if so, we call getter for each individual element. reciever[p] = value calls our setter function recursively with the new props property.

Setter for ranges with destructuring

The previous example works really well but what happens if we type l[[0, -1]] = [5, 6]; ? If you've been using numPy, you would expect to assign 5 to the first element and 6 to the last one. With our code it will nest these arrays instead:

We can easily fix it by checking if our values property is an array and if so, using the elements of an array in our forEach instead of the array itself:

Now we can make things like:

Afterword