Thursday, 16 February, 2017 UTC


Summary

A proxy is an object that wraps an object or a function and monitors access to the wrapped item, a.k.a. the target. We use proxies for the intention of blocking direct access to the target function or object.
The proxy object has some traps, handling the access of the target. The traps are the same as the methods used in the Reflect API. Therefore, this section assumes that you are familiar with the Reflect API, as you will use the same calls. The following traps are available:
  • apply
  • construct
  • defineProperty
  • deleteProperty
  • get
  • getOwnPropertyDescriptor
  • getPrototypeOf
  • has
  • isExtensible
  • ownKeys
  • preventExtensions
  • set
  • setPrototypeOf
Once a trap of a proxy object is executed, we can run any code, even without accessing the target. The proxy decides if it wants to provide you access to the target, or handle the request on its own.
The available traps allow you to monitor almost any use case. Some behavior cannot be trapped though.
We cannot tell if our object is compared to another value, or wrapped by another object. We cannot tell if our object is an operand of an operator. In reality, the absence of these use cases is hardly problematic.

Defining proxies

We can create a proxy in the following way:
let proxy = new Proxy( target, trapObject );
The first argument is target representing the proxied constructor function, class, or object.
The second argument is an object containing traps that are executed once the proxy is used in a specific way.
Let’s put our knowledge into practice by proxying the following target:
class Student {
    constructor(first, last, scores) {
        this.firstName = first;
        this.lastName = last;
        this.testScores = scores;
    }
    get average() {
        let average = this.testScores.reduce( 
            (a,b) => a + b, 
            0 
        ) / this.testScores.length;
        return average;
    }
}

let john = new Student( 'John', 'Dwan', [60, 80, 80] );
We will now define a proxy on the target object john:
let johnProxy = new Proxy( john, {
    get: function( target, key, context ) {
        console.log( `john[${key}] was accessed.` );
        // return undefined;
    } 
});
The get method of the proxy is executed whenever we try to access a property.
johnProxy.getGrade
> john[getGrade] was accessed.
> undefined

johnProxy.testScores 
> john[testScores] was accessed.
> undefined
The above defined get trap chose to ignore all the field values of the target object to return undefined instead.
Let’s define a slightly more useful proxy that allows access to the average getter function, but returns undefined for anything else:
let johnMethodProxy = new Proxy( john, {
    get: function( target, key, context ) {
        if ( key === 'average' ) {
            return target.average;
        }
    } 
});

johnMethodProxy.firstName
undefined
johnMethodProxy.average 
73.33333333333333
We can conclude that proxies can be used for the purpose of
  • defining access modifiers,
  • providing validation through the public interface of an object.
The target of proxies can also be functions.
let factorial = n =>
    n <= 1 ? n : n * factorial( n - 1 );
For instance, you might wonder how many times the factorial function was called in the expression factorial( 5 ). This is a natural question to ask to formulate an automated test.
Let’s define a proxy to find the answer out.
factorial = new Proxy( factorial, {
   apply: function( target, thisValue, args ) {
        console.log( 'I am called with', args );
        return target( ...args );
   } 
});

factorial( 5 );
> I am called with [5]
> I am called with [4]
> I am called with [3]
> I am called with [2]
> I am called with [1]
> 120
We added a proxy that traps all the calls of the factorial method, including the recursive calls. We equate the proxy reference to the factorial reference so that all recursive function calls are proxied. The reference to the original factorial function is accessible via the target argument inside the proxy.
We will now make two small modifications to the code.
First of all, we will replace
return target( ...args );
with
return Reflect.apply( 
    target, 
    thisValue, 
    args );
We can use the Reflect API inside proxies. There is no real reason for the replacement other than demonstrating the usage of the Reflect API.
Second, instead of logging, we will now count the number of function calls in the numOfCalls variable.
If you are executing this code in your developer tools, make sure you reload your page, because the existing code may interact with this
let factorial = n =>
    n <= 1 ? n : n * factorial( n - 1 );

let originalFactorial = factorial;
let numOfCalls = 0;
factorial = new Proxy( factorial, {
   apply: function( target, thisValue, args ) {
        numOfCalls += 1;
        return Reflect.apply(
            target, 
            thisValue, 
            args 
        );
   } 
});

factorial( 5 ) && numOfCalls;
> 5

Revocable proxies

We can also create revocable proxies using Proxy.revocable. This is useful when we pass proxies to other objects, but we want to keep a centralized control of when we want to shut down our proxy.
let payload = {
    website: 'zsoltnagy.eu',
    article: 'Proxies in Practice',
    viewCount: 15496
}

let revocable = Proxy.revocable( payload, {
   get: function( ...args ) {
        console.log( 'Proxy' );
        return Reflect.get( ...args );
   } 
});

let proxy = revocable.proxy;

proxy.website
> Proxy
> "zsoltnagy.eu"

revocable.revoke();

proxy.website
> Uncaught TypeError: Cannot perform 'get' on a proxy that 
> has been revoked
>    at <anonymous>:3:6
Once we revoke the proxy, it throws an error when we try using it.
As both the revoke method and proxy are accessible inside revocable, we can use the ES6 shorthand notation for objects to shorten our code:
// ...

// Create a revocable proxy
let {proxy, revoke} = Proxy.revocable( payload, {
   get: function( ...args ) {
        console.log( 'Proxy' );
        return Reflect.get( ...args );
   } 
});

// Revoke the proxy
revoke();

Use cases

When studying Proxy.revocable, we concluded that we can centralize control of data access via revocable proxies. All we need to do is pass the revocable proxy object to other consumer objects, and revoke their access once we want to make data inaccessible.
In Exercise 1, you can build a proxy that counts the number of times a method was called. Proxies can be used in automated testing. In fact, if you read the SinonJs documentation, you can see multiple use cases for proxies.
You can also build a fake server for development, that intercepts some API calls and answers them using static JSON files. For the API calls that are not being developed, the proxy simply passes the request through, and lets the client interact with the real server.
In Exercise 2, your task is to build a memoization proxy on the famous Fibonacci problem, and we will check out how many function calls we save with the lookup. If you extend this idea, you can also use a proxy to memoize the response of expensive API calls, and serve these calls without recalculating the results.
Exercise 3 highlights three use cases.
First, we may need to deal with JSON data that has a non-restricted structure. If we cannot make assumptions on the structure of a document, proxies come handy.
Second, we can establish a logging layer including logs, warning messages, and errors using proxies.
Third, if we have control over when we log errors, we can also use proxies to control client side validation.
If we combine the ideas of Exercise 2 and Exercise 3, we can use proxies to restrict access to endpoints based on user credentials. Obviously, this method does not save the server side implementation, but proxies give you a convenient way to handle access rights.

Exercises

Exercise 1. Suppose the following fibonacci implementation is given:
fibonacci = n => 
    n <= 1 ? n : 
    fibonacci( n - 1 ) + fibonacci( n - 2 );
Determine how many times the fibonacci function is called when evaluating fibonacci( 12 ).
Determine how many times fibonacci is called with the argument 2 when evaluating fibonacci( 12 ).

Exercise 2. Create a proxy that builds a lookup table of fibonacci calls, memorizing the previously computed values. For any n, if the value of fibonacci( n ) had been computed before, use the lookup table, and return the corresponding value instead of performing recursive calls.
Use this proxy, and determine how many times the fibonacci function was called
  • altogether
  • with the argument 2
while evaluating fibonacci( 12 ).

Exercise 3. Suppose object payload is given. You are not allowed to make any assumptions on the structure of payload. Imagine its contents come from the server, and the API specification may vary.
Create a proxy that executes the following statement whenever a property is accessed in the code that does not exist:
console.error( `Error: payload[${key}] does not exist.` );
You are not allowed to modify the reference to payload, and you have to use the original payload reference to access its fields.

Exercise 4. Given the object
let course = {
    name: 'ES6 in Practice',
    _price: 99,
    currency: '€',
    get price() {
        return `${this._price}${this.currency}`;
    }
};
Define a revocable proxy that gives you a 90% discount on the price for the duration of 5 minutes (or 300.000 milliseconds). Revoke the discount after 5 minutes.
The original course object should always provide access to the original price.
Learn ES6 in Practice
Sign up below to access an ES6 course with many exercises and reference solutions.