ES6 Proxies

ES6 Proxies open up some easy meta programming options.

The ES6 Proxy is now supported by most recent browser versions. A proxy wraps a target object and can intercept many of the operations on that target, for example: property access; method invocation; and construction. This opens up some simple, elegant patterns for extending the behaviour of a system without changing code, a neat way to embrace the "open/closed" principle. This post is just an brief exploration of a couple of those patterns.

Logging

It can be handy to add logging to your code. Rather than cracking open your existing (tested) code it is preferable to add logging as a separate concern. One simple pattern for doing that is to decorate existing function calls.

Let's say we had a function greet on some object target:

const target = {
  greet: (name) => `Hello ${name}`,
};

target.greet('world');
// returns 'Hello world'

Using Proxy we can add logging to target.greet (and any other functions on target) without touching its implementation; whenever a function on the target is accessed the proxy can return a function that performs the logging in addition to calling the original function:

const target = {
  greet: (name) => `Hello ${name}`,
};

const decorateIfFunction = (obj) =>
  typeof obj == 'function' ?
    decorateWithLogging(obj) :
    obj;

const decorateWithLogging = (fn) => (...args) => {
  console.log(`called ${fn.name} with [${args}]`);
  return fn(...args);
};

const proxy = new Proxy(target, {
  get: (target, prop) =>
    decorateIfFunction(target[prop]),
});

console.log( proxy.greet('world') );
// Logs "called greet with [world]",
// then "Hello world".

(JSBin example)

"Dirty" property tracking à la ORMs

If (for some reason) you were implementing an ORM library you might want to provide consumers of your library a simple API:

// Get a tracked `Person` instance
const person1 = orm.getById(Person, 1);
// Might look like: {id: 1, name: 'foo', pets: 3}
// Mutate some state using simple property assignment
person1.name = "Foo";
// Persist the changes
orm.update(person1);
// Will only update changed properties i.e. {name: 'Foo'}

That could be achieved quite simply by making the ORM return a proxy for each object. That proxy could have a special property to keep track of the dirty properties for use by the ORM:

function ORM() {
  // We will maintain a special property on each instance to keep track of
  // its dirty properties.  The property holds an object where the keys are
  // the dirty property keys.
  const dirtyPropsSymbol = Symbol('Prop holding obj where keys are dirty props');
  const resetDirtyProps  = (instance) =>       instance[dirtyPropsSymbol] = {};
  const markDirtyProp    = (instance, prop) => instance[dirtyPropsSymbol][prop] = true;
  const getDirtyProps    = (instance) =>       Object.keys(instance[dirtyPropsSymbol]);
  
  return {
    // Get an instance of `Fn` with the id of `id`
    getById: (Fn, id) => {
      // In real version we would perhaps hit an API or DB, but here we'll
      // just `new` an instance
      const instance = new Fn();
      
      // Wrap the instance in a proxy that tracks dirty props
      resetDirtyProps(instance);
      return new Proxy(instance, {
        set: (target, property, value) => {
          markDirtyProp(instance, property);
          // Perform the actual property assignment
          target[property] = value;
          return true;
        },
      });
    },
    
    // Persist and changes made on `instance`
    update: (instance) => {
      // In real version we'd perform the necessary updates to
      // persist any "dirty" properties, here we just log them
      const dirtyPropertyNames = getDirtyProps(instance);
      dirtyPropertyNames.length === 0 ?
        console.log('Nothing to update') :
        console.log('Dirty properties:', dirtyPropertyNames);

      // All properties are now persisted, so clear the list
      resetDirtyProps(instance);
    },
  };
}

(JSBin example)

Further reading:

Published on: 22 Jan 2016