Wednesday 18 November 2015

ES6 proxies part II

I already wrote about ES6 proxies in the past.. Reading this post by Dr Rauschmayer I've realised of an interesting and subtle difference depending on how we create our proxy for method interception.

In "classic" languages (C#, Java...) there are 2 strategies used by the different Proxy creation libraries. One is to use composition (the proxy class will reference the target class), and the other one is to use inheritance (the proxy class inherits from the target class). Based on the "favor composition over inheritance" principle most libraries tend to use composition. There's an interesting difference in the results of both approaches.

When using composition, if the initial method calls into another method in the object, this second call will not go through the proxy. It's normal, the proxy intercepts the first call, does its stuff, and then invokes the method through the target object. Once you are in the invokation done through the target object, the proxy has no way to intercept any ensuing code.
On the other side, when using inheritance, any secondary call goes also through the Proxy, cause indeed there is not this separation between target and proxy, your proxy is the target.

ES6 proxies are based on composition, as you have a target object and you create a proxy that points to it and intercepts actions on it. The big difference is how methods are called. A method in javascript is a property of the object, and calling a method entails 2 steps: getting it, and then invoking it. As explained in the article, ES6 proxies give you 2 traps for method calls, the get trap and the apply trap. When you use the get trap you return a function that will be later on invoked. In this returned function you put the "decorating" code, and the call to the original method. Here comes the cool part, in this function you have both the target object and the proxy (and the name of the property being intercepted), so you can invoke the property (method) either via the target (so it would be like in the composition case, no more interception happens):

//other method calls done from the first intercepted method are NOT intercepted
var entryCallProxied = new Proxy(cat, {
 //target is the object being proxied, receiver is the proxy
 get: function(target, propKey, receiver){
  //I only want to intercept method calls, not property access
  var propValue = target[propKey];
  if (typeof propValue != "function"){
   return propValue;
  }
  else{
   return function(){
    console.log("intercepting call to " + propKey + " in cat " + target.name);
    //target is the object being proxied
    return propValue.apply(target, arguments);
   }
  }
 }
});

or via the proxy itself (either using "this" or "receiver" as both point to the proxy). In this case the interception continues for other method calls performed from this returned function

//other method calls done from the first intercepted method are ALSO intercepted
var allCallsProxied = new Proxy(cat, {
 get: function(target, propKey, receiver){
  //I only want to intercept method calls, not property access
  var propValue = target[propKey];
  if (typeof propValue != "function"){
   return propValue;
  }
  else{
   return function(){
    console.log("intercepting call to " + propKey + " in cat " + target.name);
    //"this" points to the proxy, is like using the "receiver" that the proxy has captured
    return propValue.apply(this, arguments);
   }
  }
 }
});

Given the cat object below, you can see the difference between using one or another type of proxy. In the first case the call from method1 to method2 is not intercepted, while in the second case it is:

var cat = {
 name: "Kitty",
 method1: function(msg){
  console.log("cat: " + this.name + ", method1 invoked with msg: " + msg);
  this.method2(msg);
 },
 
 method2: function(msg){
  console.log("cat: " + this.name + ", method2 invoked with msg: " + msg);
 }
};

entryCallProxied.method1("Francois");

//Output:
// ------------------------------
// intercepting call to method1 in cat Kitty
// cat: Kitty, method1 invoked with msg: Francois
// cat: Kitty, method2 invoked with msg: Francois
// ------------------------------

allCallsProxied.method1("Francois");

//Output:
------------------------------
// intercepting call to method1 in cat Kitty
// cat: Kitty, method1 invoked with msg: Francois
// intercepting call to method2 in cat Kitty
// cat: Kitty, method2 invoked with msg: Francois

At the time of this writing you can run the above code in Firefox. Oddly enought, it does not work in node.js, even if we pass the --harmony_proxies flag. You can get the whole code here.

No comments:

Post a Comment