Saturday 14 December 2013

Fun with ES6 proxies

Proxies have been (and still are) a long awaited addition to JavaScript. Finally it seems like they'll be one more weapon of the ES6 arsenal. Though I remember having read information about them a few years ago, they're so new that only Firefox (always miles ahead of any other browser in terms of JavaScript innovation) implements them. You'll read that you can enable them in node.js with the --harmony-proxies argument, but that's an implementation of the old Proxy specification (Harmony Proxies: Proxy.create...) that is obsolete and has been superseded by Direct Proxies.

Ever that I've used proxies in other languages it's been for method interception, so when reading the documentation my attention was mainly drawn to the get and apply traps. It seems interesting to me that they enable 2 different approaches for the same purpose.

Let's say you have an object with just one method, something like this

var cat = {
	name: "Kitty",
	sayHiTo: function _sayHiTo(subject){
		return "Hi, " + subject + " my name is " + this.name;
	}
}; 

and you want to intercept calls to its sayHiTo method. You can wrap your cat object in a Proxy and use a get trap, so that when the method is requested (got) you return the wrapping function:

var proxiedCat = Proxy(cat, {
	get: function(target, name, receiver){
		return function(subject){
			console.log("intercepting call to method " + name + " in cat " + target.name);
			return target[name](subject);
		}
	}
});

Notice that target is the object being proxied, name is the property being requested, and receiver is the proxy itself.

We can use a different approach for this, just wrap the sayHiTo function in a Proxy and use the apply trap.

cat.sayHiTo = Proxy(cat.sayHiTo, {
	apply: function(target, thisArg, args){
		console.log("intercepting call to " + target.name + " in cat " + thisArg.name );
		return target.apply(thisArg, args);
	}
});

With the first approach you would be intercepting calls to any method in the class, so the thing seems clear to me, if you want to intercept all methods (or want to use any other trap besides get), this is the way to go. On the contrary, if you just need to intercept one (or a few) specific methods, using the second approach seems more appropriate.

The thing is that we've been intercepting (or decorating) function calls for years, just by doing something like this:

function formatString(st){
	return "X" + st + "X";
}
console.log(formatString("hi"));
console.log("formatString.name: " + formatString.name);

formatString = (function(){
	var target = formatString;
	return function formatString(st){ //notice that I'm naming the function expression, so that function.name continues to return "formatString"
		console.log("intercepting call to " + target.name);
		return target(st);
	};
})();
console.log(formatString("hi"));
console.log("formatString.name: " + formatString.name); //returns formatString

so one could wonder if there's any advantage in using "function proxies" (I mean wrapping a function in a proxy with just an apply trap:

duplicateString = Proxy(duplicateString, {
	apply: function (target, thisArg, args){
		console.log("intercepting call to " + target.name);
		return target.apply(thisArg, args);
	}
});
console.log(duplicateString("hi"));
console.log("duplicateString.name: " + duplicateString.name); 

well, the functionality is the same, so the only advantage that I can see is that the code is more semantic (the Proxy call clearly indicates that you are intercepting that function).

One very interesting point with ES6 proxies is that they try to be as transparent as possible. For example an instanceof of a proxy, or a myProxy.constructor, will work just as if we were applying it to the the proxied object (target). Also, if we proxy a function, myProxyFunction.name will return the name of the target function. This makes me wonder if there's anyway to know if a given object is a normal object or a proxy around it. So far, I haven't found a way to distinguish them.

You can see some code here, and run it from here (in Firefox+Firebug)

By the way, time ago I uploaded a small interception library to github based on the old approach.

No comments:

Post a Comment