Follow me if you want to grow your full-stack JavaScript skills with my screencasts and courses.

ES6 proxies

Resources

Google traceur transpiler: https://github.com/google/traceur-compiler

Good and up to date ES6 code samples: https://github.com/lukehoban/es6features

A compatibility table for ES6 features: http://kangax.github.io/es5-compat-table/es6/

Harmony reflect, used for ES6 proxies: https://github.com/tvcutsem/harmony-reflect

Constructing a proxy

We construct a new proxy, with two parameters, which we'll explain a bit more later on.

app.js
  
  var customer = new Proxy({}, {});

  customer.name = 'hendrik';
  customer.surname = 'swanepoel';

  console.log(customer.name);
  

Running the above code yields the expected result:

output
  
  hendrik
  [Finished in 0.6s]
  

Intercept the fields

We start doing some middleware for our normal plain old object. The first parameter to the constructor is the object being proxied, and the second one is the proxy itself where we get to add our hooks for our getters and setters.

app.js
  
  var customer = new Proxy({}, {
    get: function(target, name, receiver) {
      console.log('get called for field: ', name);
      return Reflect.get(target, name, receiver);
    },
    set: function(target, name, value, receiver) {
      console.log('set called for field: %s, and value: %s', name, value);
      return Reflect.set(target, name, value, receiver);
    }
  });

  customer.name = 'hendrik';
  customer.surname = 'swanepoel';
  

By adding our middleware on the objects, we get the following results:

output
  
  set called for field: name, and value: hendrik
  set called for field: surname, and value: swanepoel
  [Finished in 0.6s]

  

Our own objects

We don't want to construct a new proxy everytime we want to work with our customer logic, so we wrap the proxy definition inside of our own Customer constructor function.

app.js
  

  function Customer() {

    var proxy = new Proxy({}, {
      get: function(target, name, receiver) {
        console.log('get called for field: ', name);
        return Reflect.get(target, name, receiver);
      },
      set: function(target, name, value, receiver) {
        console.log('set called for field: %s, and value: %s', name, value);
        return Reflect.set(target, name, value, receiver);
      }
    });

    return proxy;
  }

  var customer = new Customer();
  customer.name = 'hendrik';
  customer.surname = 'swanepoel';
  

Adding a save function

Now we add a save function on to our customer object. Remember, the first argument to the proxy constructor is the object being extended, so we add a save function on to that:

app.js
  

  function Customer() {

    var proxy = new Proxy({
      save: function(){
        console.log('proceeding with expensive saving operation!');
      },
    }, {
      set: function(target, name, value, receiver) {
        return Reflect.set(target, name, value, receiver);
      }
    });


    return proxy;
  }

  var customer = new Customer();
  customer.name = 'hendrik';
  customer.surname = 'swanepoel';
  customer.save();
  

Running that yields the following:

output
  
  proceeding with expensive saving operation!
  [Finished in 0.6s]
  

Only save when data changed

As our log statement states, the save operation is expensive, so we only want to run it if the data actually changed. This we do by setting a dirty flag in our set hook, which the save method can use to decide whether it should proceed with the save logic.

app.js
  

  function Customer() {

    var proxy = new Proxy({
      save: function(){
        if(!this.dirty){
          return console.log('Not saving, object still clean');
        }

        console.log('proceeding with expensive saving operation!');
      },
    }, {
      set: function(target, name, value, receiver) {
        target.dirty = true;
        return Reflect.set(target, name, value, receiver);
      }
    });


    return proxy;
  }

  var customer = new Customer();
  customer.save();
  customer.name = 'hendrik';
  customer.save();

  

Running that yields the following:

output
  
  Not saving, object still clean
  proceeding with expensive saving operation!
  [Finished in 0.6s]
  

Keeping track of changed fields

We now only proceed with save logic if any of the fields changed, but we can go even further and keep track of the exact fields that were changed:

app.js
  

  function Customer() {

    var proxy = new Proxy({
      save: function(){
        if(!this.dirty){
          return console.log('Not saving, object still clean');
        }

        console.log('proceeding with expensive saving operation: ', this.changedProperties);
      },
    }, {
      set: function(target, name, value, receiver) {
        target.dirty = true;
        target.changedProperties = target.changedProperties || [];
        if(target.changedProperties.indexOf(name) == -1){
          target.changedProperties.push(name);
        }
        return Reflect.set(target, name, value, receiver);
      }
    });


    return proxy;
  }

  var customer = new Customer();
  customer.name = 'hendrik';
  customer.surname = 'swanepoel';
  customer.save();


  

Running that yields the following:

output
  
  proceeding with expensive saving operation:  [ 'name', 'surname' ]
  [Finished in 0.6s]
  

Simulating an atomic MongoDB query

We pull in lodash and use the pick command to build up an object containing the fields that were changed, then use that to simulate an atomic MongoDB update command (updating only the fields that changed in the db):

app.js
  
  var _ = require('lodash');

  function Customer() {

    var proxy = new Proxy({
      save: function(){
        if(!this.dirty){
          return console.log('Not saving, object still clean');
        }

        console.log('proceeding with expensive saving operation!');
        var updateCmd = _(this).pick(this.changedProperties).value();
        console.log('db.customers.update({_id: 5}, {$set: %s})', JSON.stringify(updateCmd));
      }
    }, {
      get: function(target, name, receiver) {
        return Reflect.get(target, name, receiver);
      },
      set: function(target, name, value, receiver) {
        target.dirty = true;
        target.changedProperties = target.changedProperties || [];
        if(target.changedProperties.indexOf(name) == -1){
          target.changedProperties.push(name);
        }
        return Reflect.set(target, name, value, receiver);
      }
    });


    return proxy;
  }

  var customer = new Customer();
  customer.name = 'hendrik';
  customer.surname = 'swanepoel';
  customer.save();

  

Running that yields the following:

output
  
  proceeding with expensive saving operation!
  db.customers.update({_id: 5}, {$set: {"name":"hendrik","surname":"swanepoel"}})
  [Finished in 0.5s]
  

Wrap up

As you can see proxies are tremendously powerful, getting rid of gunk like obj.set('key', value) in apis, and instead allows authors to use middleware.