Extending arrays using proxies in JavaScript
Tags: javascript, node, proxies, arrays
Published: 2019-04-01 23:44:41

> Originally posted at [michaelzanggl.com](https://michaelzanggl.com/articles/extending-arrays-using-proxies). Subscribe to [my newsletter](https://michaelzanggl.com/) to never miss out on new content.

In the last post we were looking at subclassing arrays. This time let's check out another feature ES6 brought to the table. Proxies!

We will continue in the spirit of test driven development. If you haven't read the 
previous post, we basically installed mocha and chai for testing and have a `src` as well as a `test` folder.

The goal is to have dynamic method names on our arrays so we can basically integrate any library.

Let me introduce to you `roxy`. You can find the GitHub [here](https://github.com/MZanggl/roxy).

Before I even explain what proxies are, let's take a look at the test so you have a better picture of what we are trying to achieve.

> Be aware that due to the nature of proxies, they are not polyfillable.

In the last post we were integrating the pluck method first. In case you are unfamiliar with it, here is the ES6 way of doing it

```javascript
const locations = [
  { city: 'Tokyo' },
  { city: 'Naha' },
]

// pluck in ES6
const cities = locations.map(loc => loc.city)
cities //? [ 'Tokyo', 'Naha']
```

The goal was to turn `locations.map(loc => loc.city)` into `locations.pluck('city')`.

To help us achieve the goal and add many other useful methods at the same time, let's integrate lodash.

In order to pluck using lodash you use lodash's own `map` method. If you pass a string instead of a function as the second argument, that string becomes the key it will pluck.

```javascript
_.map(locations, 'city')
```

So here are the tests

```javascript
const expect = require('chai').expect
const { proxy } = require('../src/roxy')

describe('proxy', function() {
    it('should be able to access Array.prototype methods', function() {
        const numbers = proxy([1, 2, 3])
        numbers.copyWithin(0, 1, 2) // lodash doesn't have `copyWithin`
        expect(numbers).to.deep.equal([ 2, 2, 3 ])
    })

    it('should pluck using lodash method', function() {
        const numbers = proxy([
            { id: 1 },
            { id: 2 },
            { id: 3 },
        ])

        const result = numbers.map('id')
        expect(result).to.deep.equal([ 1, 2, 3 ])
    })
})
```

That's pretty cool. We got rid of the `new` keyword and classes altogether. Let's check out `roxy.js`.

```javascript
// let's only load the lodash methods we need
var lodashArray = require('lodash/array')
var lodashCollection = require('lodash/collection')

const proxySymbol = Symbol('isProxy')

function _transformResult(result) {
    if (Array.isArray(result) && !result[proxySymbol]) {
        return proxy(result)
    }
    return result
}

const libraryList = [lodashCollection, lodashArray]

function proxy(array) {
    const handler = {
        get: function get(target, prop) {
            const library = libraryList.find(library => typeof library[prop] === 'function')
            if (library) {
                return function(...args) {
                    const result = library[prop](this, ...args)
                    return _transformResult(result)
                }
            }

            if (typeof target[prop] !== 'undefined') {
                return target[prop]
            }

            if (prop === proxySymbol) {
                return true
            }
        }
    }
    
    return new Proxy(array, handler)
}

module.exports = {
    proxy,
}
```

Proxies are exactly what their name suggest. We could also say it is a gateway to the actual array. But the gateway is guarded by so called `traps`. In this case, whenever you access a property on the array, it will not actually access it, but it will get `trapped` in the `get` method of our handler, so let's go through it step by step to understand it.

```javascript
const library = libraryList.find(library => typeof library[prop] === 'function')
if (library) {
    return function(...args) {
        const result = library[prop](this, ...args)
        return _transformResult(result)
    }
}
```

First, we check if the method is either inside lodash collections or lodash arrays.

If you access `array.map` it will find it inside `lodashCollection` and return a new function. The function already knows at that point that `library[prop]` will be `lodashCollection.map`.

Then, when you execute the function like this `array.map('id')`, it will take the arguments you passed and executes the lodash function together with the actual array as the first argument.

With `_transformResult` we will proxy the result again in case it is a normal array. This allows for better chaining.

---

```javascript
if (typeof target[prop] !== 'undefined') {
    return target[prop]
}
```

Next, we want to check if the method is an existing property of the array and simply return it. This would be the case for accessing the `length` property or methods like `copyWithin` that don't exist in lodash.

```javascript
if (prop === proxySymbol) {
    return true
}
```

This lets us know whether an array is a roxy instance or not. In `_transformResult` when we access `result[proxySymbol]` and `result` is already a roxy instance, it would get trapped in the `get` method and would return true at this point `if (prop === proxySymbol)`. So in case the returned array is already a roxy instance, no need to proxy it again.

Check out the part of `_transformResult` again:

```javascript
if (Array.isArray(result) && !result[proxySymbol]) {
    return proxy(result)
}
return result
```

We can check if `_transformResult` works by writing another test

```javascript
it('should be able to chain lodash methods', function() {
    const locations = proxy([
        { location: {city: 1 } },
        { location: {city: 2 } },
        { location: {city: 3 } },
    ])

    const result = locations.map('location').map('city')
    expect(result).to.deep.equal([ 1, 2, 3 ])
})
```

The same way you can now use lodash's `map` method, you should be able to use a bunch more like `chunk`, `keyBy`, `shuffle` and so on.

Of course you don't have to use `lodash`. You can do something similar with any array library. But there is probably not a single library that would fulfill all your expecations. So let's also add a method to create custom macros.

The test

```javascript
const { proxy, macro } = require('../src/roxy')

// ...

it('can use macros', function() {
    macro('stringify', (array, prepend) => prepend + JSON.stringify(array))

    const numbers = proxy([1, 2])
    const result = numbers.stringify('array: ')
    expect(result).to.equal('array: [1,2]')
})
```

For the implementation, we only have to do a couple things.

```javascript
const macroMap = {}
const libraryList = [lodashCollection, lodashArray, macroMap]

function macro(name, fn) {
    macroMap[name] = fn
}

// ...

module.exports = {
    proxy,
    macro,
}
```

And that's all there is to it!

## Conclusion

Proxies offer a wide range of new possibilities to explore. In fact the [next major version of vue will use proxies for its reactivity system](https://medium.com/vue-mastery/evan-you-previews-vue-js-3-0-ab063dec3547). This tutorial made only use of the `get` trap. There are actually many more like `set`, `construct`, `has`, etc. Check out the mdn references below to learn more about proxies.


## References
- https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy
- https://github.com/MZanggl/roxy
- https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol