One feature of the current JavaScript language standard, ECMAScript 5, that hasn't gotten nearly enough attention is the Object.defineProperty() function. This function is what makes modules like mongoose possible. If you've never used Object.defineProperty() before, here's a simple use case: suppose you wanted to be able to increment and decrement a JavaScript date's month property using the ++ and -- operators.

var d = new Date();

Without Object.defineProperty() this task would be impossible outside of functions. With Object.defineProperty(), this is easy:

var d = new Date();
// Thu Apr 23 2015 19:23:46 GMT-0300 (UYT)
console.log(d);
Object.defineProperty(d, 'month', {
  get: function() {
    return d.getMonth();
  },
  set: function(v) {
    d.setMonth(v);
  }
});

++d.month;
// Sat May 23 2015 19:23:46 GMT-0300 (UYT)
// Exactly one month later!
console.log(d);

You may be wondering why I'm discussing Object.defineProperty() in a post about proxies. Turns out, proxies are just a more general form of Object.defineProperty().

What are Proxies?

In the previous section, you saw some of the cool syntactic tricks that Object.defineProperty() makes possible. The Object.defineProperty() function is just one way you can hook in to the ECMAScript GetProperty() method. If you're not the kind of person that peruses the ECMAScript spec for fun, just be aware that when you do a property access, like obj.prop, the JavaScript interpretter allows you to hook functions into this property access.

Proxies are the natural extension to Object.getProperty(). Note that Object.defineProperty() requires you to specify the property name. Proxies allow you to define a catch-all function that executes for all property accesses on an object. A basic cute example of how proxies work is an object that enables you to convert a string to uppercase with a property access:

var uppercase = Proxy.create({
  get: function(proxy, key) {
    return key.toUpperCase();
  }
});

// Outputs "A"
console.log(uppercase.a);
// Outputs "B"
console.log(uppercase.b);

Note The above code will only run in Node 0.12 with the --harmony_proxies flag. The --harmony flag is not sufficient to enable proxies in NodeJS.

Using Setters with Proxies

Proxies are not just getter-only, you can also define a set function which integrates with assignment operators (as well as increment and decrement). Setters are tricky with proxies though, because a proxy getter executes for every property access. Thus, a proxy getter that accesses this is actually recursive and so it's easy to trigger an infinite recursion.

Because of the infinite recursion, typically you don't want to use this in a proxy setter. The most simple choice is to use a proxy as a wrapper around a non-proxy object in the proxy's scope. For instance, let's say you want to store an array of dates, but you want to be able to interact with the dates as if they were strings.

var moment = require('moment');
var arr = [];

var p = Proxy.create({
  get: function(proxy, key) {
    var i = parseInt(key, 10);
    if (isNaN(i)) {
      return arr[key];
    }
    if (arr[i]) {
      return arr[i].format('YYYY-MM-DD');
    }
    return arr[i];
  },
  set: function(proxy, key, value) {
    var i = parseInt(key, 10);
    if (isNaN(i)) {
      return arr[key] = value;
    }
    arr[i] = moment(value, 'YYYY-MM-DD');
  }
});

p[0] = '2011-06-01';
// Prints "string"
console.log(typeof p[0]);
// Prints "object"
console.log(typeof arr[0]);

// Prints "2011-06-01 is a Wednesday"
console.log(p[0] + ' is a ' + arr[0].format('dddd'));
p[0] = '2012-06-01';
// Prints "2012-06-01 is a Friday"
console.log(p[0] + ' is a ' + arr[0].format('dddd'));

In the above example the proxy p sits in front of the array of moment objects arr. Any set operation on the proxy gets translated by the set function above into setting a moment object in arr.

Note if you declare an array with var arr = [1]; and access using a numeric string, JavaScript will convert it to a number. For instance, arr['0'] === arr[0]. That's why the parseInt() logic above gives the correct behavior. I'm not entirely sure why this is the case and I'd be quite happy if somebody could point me to the relevant part of the ECMAScript spec.

Edit Sept 25, 2015: According to section 6.1.7 of the ES6 spec, array indexes are always interpretted as strings.

Proxies Gotchas

Here's an interesting question - what happens when you call require('util').inspect() on the proxy p in the previous example? If you guessed "node crashes horribly with a baffling error message", you'd be right.

util.js:236
  var keys = Object.keys(value);
                    ^
TypeError: undefined is not a function
    at Function.keys (native)
    at formatValue (util.js:236:21)
    at Object.inspect (util.js:147:10)

Object.keys(p) crashes out with a very strange error - Object.keys is definitely not undefined. This is a problem with how the proxy was implemented. Because a proxy has full control over property accesses, there's no way for node to figure out what properties the proxy has other than trying every possible string. To be able to inspect the proxy properly, you need to define the getOwnPropertyNames and getOwnPropertyDescriptor "traps":

var p = Proxy.create({
  get: function(proxy, key) {
    var i = parseInt(key, 10);
    if (isNaN(i)) {
      return arr[key];
    }
    if (arr[i]) {
      return arr[i].format('YYYY-MM-DD');
    }
    return arr[i];
  },
  set: function(proxy, key, value) {
    var i = parseInt(key, 10);
    if (isNaN(i)) {
      return arr[key] = value;
    }
    arr[i] = moment(value, 'YYYY-MM-DD');
  },
  // Return a list of property names
  getOwnPropertyNames: function() {
    return Object.keys(arr);
  },
  // Return the descriptor for the given property. Note that the first parameter
  // is **not** 'proxy', at least in NodeJS
  getOwnPropertyDescriptor: function(key) {
    return Object.getOwnPropertyDescriptor(arr, key);
  }
});

With these two traps, require('util').inspect(p); works as you might expect, modulo the fact that p is not an array.

{ '0': 
   { [Number: 1338519600000]
     _isAMomentObject: true,
     _i: '2012-06-01',
     _f: 'YYYY-MM-DD',
     _isUTC: false,
     _pf: 
      { empty: false,
        unusedTokens: [],
        unusedInput: [],
        overflow: -1,
        charsLeftOver: 0,
        nullInput: false,
        invalidMonth: null,
        invalidFormat: false,
        userInvalidated: false,
        iso: false },
     _locale: 
      { _ordinalParse: /\d{1,2}(th|st|nd|rd)/,
        ordinal: [Function],
        _abbr: 'en',
        _ordinalParseLenient: /\d{1,2}(th|st|nd|rd)|\d{1,2}/ },
     _d: Fri Jun 01 2012 00:00:00 GMT-0300 (UYT),
     _isValid: true } }

Conclusion

Proxies are a pretty exciting feature in ECMAScript 6. For one thing, we'll finally be able to implement syntactic sugar on top of array accesses. Proxies are currently pretty unstable (MDN and NodeJS still disagree on function signatures), but as they stabilize they'll enable some very sweet JavaScript. Just like how Object.defineProperty() made mongoose possible, proxies will enable modules that work like magic.

Found a typo or error? Open up a pull request! This post is available as markdown on Github
comments powered by Disqus