Fun with JavaScript Proxies

Disclaimer: This article is for educational purposes only. In several of these fun examples, I extend native objects. I don't advocate doing that unless you have consensus amongst your team and nobody else's code will depend upon yours.

Bend JavaScript to your Will

There are more useful applications of the JavaScript Proxy than what I'm about to show you, but having a little fun makes the learning that much better. Plus, these are simple proxies that simply are shortcuts to other, existing method. I think that shows off the concept more clearly.

Therefore, I present to you my shortcuts for built-in method names that I hate typing.

listen

I get tired of writing addEventListener, so I created a new method on EventTarget.prototype named listen and then proxy to the longer form.

if (!("listen" in EventTarget.prototype)) {
  Object.defineProperty(EventTarget.prototype, "listen", {
    value: new Proxy(EventTarget.prototype.addEventListener, {
      apply: function (_target, _this, _args) {
        return _target.apply(_this, _args);
      }
    })
  });
}

shift & pop

If you're like me, you never remember what the hell shift() and unshift() do on an array. How about we proxy a new method named first() to shift().

if (!("first" in Array.prototype)) {
  Object.defineProperty(Array.prototype, "first", {
    value: new Proxy(Array.prototype.shift, {
      apply: function (_target, _this, _args) {
        return _target.apply(_this, _args);
      }
    })
  });
}

let first = [1,2,3,4].first();  // 1

And last() for pop().

if (!("last" in Array.prototype)) {
  Object.defineProperty(Array.prototype, "last", {
    value: new Proxy(Array.prototype.pop, {
      apply: function (_target, _this, _args) {
        return _target.apply(_this, _args);
      }
    })
  });
}

let last = [1,2,3,4].last();  // 4

def & property

I'm a huge fan of Python. I enjoy it almost as much as I enjoy JavaScript. I also like that the two languages are converging syntactically.

But I'm an impatient person.

I present to you the JavaScript versions of def and @property. Both are enumerable. Properties created with property are writable, and configurable. Methods created with def are non-writable, non-configurable.

let pyscript = Object.create(null);

/*
  pyscript allows any object to define its own properties and methods.
  Chainable.

  Usage:
    let foo = Object.create(pyscript.ObjectExtensions);
    foo.property("bar", 1).property("baz", 2).def("bam", () => ({}));
 */
pyscript.ObjectExtensions = (() => {
  let o = Object.create(null);

  // Used for defining a writable/enumerable property
  o.property = new Proxy(Object.defineProperty, {
    apply: function (_target, _this, _args) {
      _target(_this, _args[0], {
        value: _args[1],
        configurable: true,
        writable: true,
        enumerable: true
      });

      return _this;
    }
  });

  // Used for defining an enumerable function
  o.def = new Proxy(Object.defineProperty, {
    apply: function (_target, _this, _args) {
      _target(_this, _args[0], {
        value: _args[1],
        enumerable: true
      });

      return _this;
    }
  });

  return Object.freeze(o);
})();

Object.freeze(pyscript);

When I am creating an object that wants to use these helper proxies, I simply specify it as the prototype. This example uses the WeakMap pattern for storing private data, and the fun random native object extensions I wrote.

const _internal = gutil.privy.init(); // Private store

// Use pyscript object proxies
let MasterSpell = Object.create(pyscript.ObjectExtensions);

// String representation of master spell
MasterSpell.def("toString", function () {
  return `${ this.label } of ${ this.elements.random() }`;
});

/*
  Reading the spell modifies the damage based on 
  the mage's intelligence
*/
MasterSpell.def("read", function (modifier) {
  this.intelligence_modifier = modifier;
  return this;
});

/*
  Casting the spell calculates total effect.
*/
MasterSpell.def("cast", function () {
  this.effect = Math.round(Math.random() * this.base_effect + this.effect_modifier);
  return this;
});

MasterSpell.property("name", "").property("target", null)
           .property("effect", 0).property("base_damage", 0);

random

In this example, the code modifies the behavior of a native object, but only by using a dynamic proxy instead of extending the native object itself.

The randomize function will take an Array, Map or Set as its input, and returns a proxy that overrides the default get functionality. It also converts the Map or Set into an array to return a random item.

Again, no practical value, but it's a fun example that produces some unexpected behaviors.

// Pass in an Array, Map, or Set object
const randomize = (object) => {

  // Define the handler for the proxy
  const handler = {

    // Override the default behavior of getting an item from the collection
    get (target, property) {

      /*
        If an array was passed in, return a random item, regardless of which
        index is requested.
      */
      if (Array.isArray(object)) {
        return target[Math.floor(Math.random() * target.length)];

      // Convert Map or Set to array, using spread syntax, and return random item
      } else {
        const convert = [...target.values()];
        return convert[Math.floor(Math.random() * convert.length)];
      }
    }
  };

  // Generate and return the new proxy
  const proxy = new Proxy(object, handler);
  return proxy;
};

// Proxy an array.
var baz = randomize([1,2,3,4,5,6,7,8,9]);
console.log("random array item", baz[0]);  // 5
console.log("random array item", baz[0]);  // 1
// I can even request an index that doesn't exist!
console.log("random array item", baz[999]);  // 4


// Populate a Map
var foo = new Map();
foo.set({name:"Monster"}, {age: 99});
foo.set({name:"Skeleton"}, {age: 11});
foo.set({name:"Vampire"}, {age: 555});

// Proxy the Map
foo = randomize(foo);
console.log("random map item", foo[0]);  // {age: 555}
console.log("random map item", foo[0]);  // {age: 99}

// Populate a Set
var bar = new Set();
bar.add("earth");
bar.add("wind");
bar.add("fire");

// Proxy the Set
bar = randomize(bar);
console.log("random set item", bar[null]);  // fire
console.log("random set item", bar[-1]);  // fire
console.log("random set item", bar[12]);  // earth