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