?
Digital Application Development
7 minute read

Exploiting JavaScript Proxies for Fun: Part 4 - Proxied Property Procurement

Proxies were added to JavaScript in 2015, but still are a rare sight when developing. This article series will take a look at why that might be and some of the fun ways proxies can be used to enhance your code.

In This Article

In the first part of this series, we looked at what JavaScript proxies are and a simple use case for validating keys on an object. We have also looked at using a proxy to allow for private properties in JavaScript classes as well as using a proxy for reactive coding. For the final part of this series, we'll look at combining functional programming and proxies to emulate a Scala language feature. As a reminder, all code in this article series is available on GitHub.

The problem to solve

A common task performed in JavaScript is extracting the same property from each item in an array. Let's set up some sample data:

const tests = [
    { student: 'Fred Jones', percentage: 82, answers: [5, 6, 7, { passion: 'traps' }] },
    { student: 'Daphne Blake', percentage: 91, answers: [5, 6, 7, { passion: 'fashion' }] },
    { student: 'Velma Dinkley', percentage: 106, answers: [5, 6, 7, { passion: 'intellect' }] },
];

For this set of data, let's calculate the average score on the test:

const assert = require('assert');

const average = (nums) => nums.reduce((total, num) => total + num, 0) / nums.length;

const scores = tests.map((test) => test.percentage);

assert.strictEqual(average(scores), 93);

We use a lambda function to extract each test's percentage value and then use this list of numbers to calculate the average. Simple enough.

Writing a new lambda every time we need to do a simple operation like this can feel wasteful or like there's a little too much boilerplate (even though it is well below that required by other languages). Fortunately, we can look to functional programming for a solution that simplifies things a bit:

const getProp = (prop) => (obj) => obj[prop];

const scores = tests.map(getProp('percentage'));

assert.strictEqual(average(scores), 93);

Now instead of creating a new lambda each time, we can use our getProp function to create one for us by simply passing it the key we want to extract! The downside to this approach is that it may be confusing to newer developers who have not encountered functions returning other functions, but for the most part, it is a clear and concise utility.

But what does this have to do with proxies?

copy link

Drawing inspiration from Scala

It's always a good idea to look at other programming languages and communities for inspiration and ideas of how to make your code more readable and writeable. When looking at Scala, we find a feature that allows developers to avoid having to write lambdas in this type of situation called the placeholder syntax:

listOfObjects.map(_.property); // Scala

The placeholder syntax actually allows for more flexibility than we can mimic in JavaScript, including using operators, like the following:

listOfObjects.filter(_.property > 5); // Scala

This is impossible due to JavaScript's type coercion and inability to overload operators. But we can emulate the simple property accessing example using a proxy. We'll do so by creating a constant _ that uses the get function in the proxy handler to return a call of the getProp function we defined earlier:

const placeholderHandler = {
    get(target, prop) {
        return getProp(prop);
    }
};

const _ = new Proxy({}, placeholderHandler);

const scores = tests.map(_.percentage);

assert.strictEqual(average(scores), 93);

That was easy! And it looks like straight-up magic. We no longer have to directly call getProp, we can just use our proxy. But data is hardly ever so neat as to be only one layer deep. In functional programming, we can address this fairly easily by creating a new getDeepProp function so that we can pass an arbitrary amount of properties to dive down to:

const getDeepProp = (...props) => (obj) => {
    for (const key of props) {
        obj = obj[key];
    }

    return obj;
};

const passions = tests.map(getDeepProp('answers', 3, 'passion'));

assert.deepStrictEqual(passions, ['traps', 'fashion', 'intellect']);

That seems to work fairly well, but how can we use it with our placeholder proxy? We can only get a single property at a time in our get handler, so we're going to have to get a little more clever. We can do two things: create new proxies on the fly, and use the apply function in our proxy handler.

We'll start with creating new proxies on the fly. We need to use our get handler an arbitrary amount of times so that each key can be captured. To do this, we will switch from calling getProp to returning a new proxy:

const placeholderHandler = {
    get(target, prop) {
        return new Proxy({}, placeholderHandler);
    }
};

We can now call our proxy to any depth, but that doesn't do us much good when we need to actually make a function call eventually. It also doesn't help that right now, we have no idea what property we were looking at last, let alone the whole chain. To fix this, we can store a path property on our target that adds each new call to get:

const placeholderHandler = {
    get(target, prop) {
        return new Proxy({ path: [...target.path, prop] }, placeholderHandler);
    }
};

const _ = new Proxy({ path: [] }, placeholderHandler);

const passions = tests.map(_.answers[3].passion); // ERROR: not a function

We still can't treat our proxy as a function though. To change this, we can add our apply handler to call our new getDeepProp and…

const placeholderHandler = {
    apply(target, thisArg, args) {
        return getDeepProp(...target.path)(args[0]);
    },
    get(target, prop) {
        return new Proxy({ path: [...target.path, prop] }, placeholderHandler);
    }
};

const passions = tests.map(_.answers[3].passion); // ERROR: not a function

…oh, right. Proxies require the underlying target to still be a callable function in order to use the apply function. We'll have to make some tweaks, but fortunately, JavaScript's flexibility helps us make this easier since we can set properties on functions. We'll create a small helper function for creating our new targets which will be simple noop functions with a path property:

const createNoop = (path) => {
    const noop = () => {};
    noop.path = path;

    return noop;
};

const placeholderHandler = {
    apply(target, thisArg, args) {
        return getDeepProp(...target.path)(args[0]);
    },
    get(target, prop) {
        return new Proxy(createNoop([...target.path, prop]), placeholderHandler);
    }
};

const _ = new Proxy(createNoop([]), placeholderHandler);

const passions = tests.map(_.answers[3].passion);

assert.deepStrictEqual(passions, ['traps', 'fashion', 'intellect']);

Awesome! We've found all of our students' passions! But, we're not quite done. We can actually simplify this so that we avoid adding properties to functions, which tend to look very messy. Notice that all we're doing in the apply function is calling another function. What if we used a more purposeful function for our target instead of a noop?

We can do this using the original getProp function and a new identity function. By having each successive get call return a function that first calls the previous target and then extracts its own desired prop, we can effectively use these closures to avoid the apply handler altogether:

const identity = (a) => a;

const placeholderHandler = {
    get(target, prop) {
        return new Proxy((obj) => target(obj)[prop], placeholderHandler);
    }
};

const _ = new Proxy(identity, placeholderHandler);

const passions = tests.map(_.answers[3].passion);

assert.deepStrictEqual(passions, ['traps', 'fashion', 'intellect']);

The identity function allows the _ itself to act as the original object in the call structure, as it just returns the object itself. Now our proxy definition is much simpler by avoiding both the need to attach properties to functions, relying on closures instead and no longer needing the apply function!

There is, however, a reason to choose the other approach. While retrieving properties at arbitrary depth is cool, we can actually work to support even more functionality. For an example of taking this further, you can check out the code sample, where our placeholder can allow for method calls, argument rearrangement, spreading and collecting of arguments and even deeply extracting from a nested array.

That's all I have on proxies for now! Hopefully you were able to learn something new. If you have any other ideas for how proxies could be used, please feel free to add your thoughts in the comment section below.