Adding Logging To Objects With Help Of Metaprogramming

Metaprogramming in JS is not as much as big buzz word like say in Ruby. Partly perhaps because the whole experience of coding in JavaScript is one big meta-programming experience. But sometimes there are those small little hacks that make our life as developers easier, and I think “metaprogramming” is just the right term to describe them.

Let’s say you want to modify the behaviour of an existing Backbone view.

Have you ever had this feeling. You look at someone’s code. And you’re just trying to understand, what’s happening there. What methods are being invoked, and in what particular order. Wouldn’t it be nice if Backbone could log every method being invoked into console, maybe with some meta information like when it started, when it finished, how long did it take, etc.?

Well, there’s a really easy way to do this.

Let’s say, we have a Backbone view with several binded events, lot’s of methods and stuff. Let’s create a function that adds logging capability to it. It takes a view, and makes methods log themselves.

Naturally I call it ‘logify’.

function logify(cl) {
}

First of all we should know the names of methods. Methods reside in prototype property because a Backbone view is a constructor function. So we should take getOwnPropertyNames and then take only functions from it.

function logify(cl) {
  // Take the prototype, cause it's where all the methods reside
  var proto = cl.prototype;
  // Take owned methods
  var owned = Object.getOwnPropertyNames(proto);
  var methods = owned.filter(function (x) { return typeof proto[x] == 'function' });
};

OK, here’re we go. Now let’s replace the old methods with the new ones. The ones that would take log something to console and then invoke the old methods.

function logify(cl) {
  // ...
  // Let's replace every method with a simple wrapper
  methods.forEach(function (n) {
    var old = proto[n]; // old method
    proto[n] = function() {
      console.log("-> " + n + " invoked");
      res = old.apply(this, arguments); // call the old fn in context of current object and with the arguments
      return res; // don't forget to return the value
    };
  });
};

There are two things to notice. First, we can’t just invoke the old methods. We should pass to it both the arguments and the this context. We’re doing it with help of apply function. Second, we shouldn’t forget to return the value produced by the old method.

Now, as it’s done, it’s easy to add some additional logic to it. Here’s an example of logify logging the time took for method to execute.

function logify(cl) {
  // Take the prototype, cause it's where all the methods reside
  var proto = cl.prototype;
  // Take owned methods
  var owned = Object.getOwnPropertyNames(proto);
  var methods = owned.filter(function (x) { return typeof proto[x] == 'function' });
  // Let's replace every method with a simple wrapper
  methods.forEach(function (n) {
    var old = proto[n]; // old method
    proto[n] = function() {
      var tick = Date.now();
      console.log("-> " + n + " started");
      res = old.apply(this, arguments); // call the old fn in context of current object and with the arguments
      var tack = Date.now();
      var d = (tack - tick) / 1000;
      console.log("-> #{n} exited in #{d}s");
      return res; // don't forget to return the value
    };
  });

And that’s about it. Now we can take any view (or actually any ‘class’) and make it log itself:

logify(MyView);

See ya!

 
6
Kudos
 
6
Kudos

Now read this

11 JavaScript Blogs to Follow in 2017

There’s a huge benefit in running a JavaScript newsletter. You get to know about many smart people from whom you can learn a lot. Here are the most interesting blogs I stumbled upon. Feel free to add them directly to your preferred RSS... Continue →