Magic Arguments

In this post I am going to talk about an interesting pattern in Javascript which I will call: magic arguments. I will give two examples of where it's been used.

$super argument in Prototype

The Prototype library uses this technique to implement the $super special argument - which an overriding method can use to reference the method of the same name implemented by the super class.

For instance, this is how you'd write a class and methods using Prototype

var Person = Class.create({
  initialize: function(name) {
    this.name = name;
  },
  say: function(message) {
    return this.name + ': ' + message;
  }
});

If you override the say method to say something else, this is what you'd do

var Pirate = Class.create(Person, {
  // redefine the speak method
  say: function(message) {
    return this.name + ': ' + message + ', yarr!';
  }
});

And, this is how you would override the say method and allow it to call the say method of Person:

var Pirate = Class.create(Person, {
  // redefine the speak method
  say: function($super, message) {
    return $super(message) + ', yarr!';
  }
});

Now, you should be suspicious at this point, because: how is it that when $super is the first argument, it knows to supply the super-method as the first argument rather than message - as it normally would be the case?

If you peek into the source and searched for $super you'd find the answer

if (ancestor && Object.isFunction(value) &&
          value.argumentNames()[0] == "$super") {

Here, value is a function(the first part of the if verifies that), and the method value.argumentNames() is called to get at the argument names of that function. Then with that information at hand, it does a conditional to say: if the name of the first argument is $super, insert the overridden method as the first argument, otherwise just leave the argument list alone. The code for argumentNames is actually not that bad, it is this

function argumentNames() {
  var names = this.toString().match(/^[\s\(]*function[^(]*\(([^)]*)\)/)[1]
    .replace(/\/\/.*?[\r\n]|\/\*(?:.|[\r\n])*?\*\//g, '')
    .replace(/\s+/g, '').split(',');
  return names.length == 1 && !names[0] ? [] : names;
}

Basically, calling the toString() method on a function returns the actual Javascript source code for that function (try it!) - from which you can parse out the argument names.

For reference, here is more on class-style inheritance in Prototype. Now, let's move on to the next example.

Mocha

The Mocha Test Framework also uses magic arguments as part of its arsenal. In Mocha, a test function can either be run synchronously or asynchronously. Let's see them both. First, sync

describe('Array', function(){
  describe('#indexOf()', function(){
    it('should return -1 when the value is not present', function(){
      [1,2,3].indexOf(5).should.equal(-1);
      [1,2,3].indexOf(0).should.equal(-1);
    })
  })
})

Now async

describe('User', function(){
  describe('#save()', function(){
    it('should save without error', function(done){
      var user = new User('Luna');
      user.save(function(err){
        if (err) throw err;
        done();
      });
    })
  })
})

Can you tell the difference? Right! The async version has a done argument in the test function where as the sync version has no arguments. (Sorry, been watching too much Dora the Explorer). In the async version, the done callback must be called at some point by the test code to signal the end of the test, but that's not true of the sync version - which just ends when the function finishes executing.

So, the presence of the argument is important. As you'd find out if you try to write a sync test, but left a done argument in there

describe('Array', function(){
  describe('#indexOf()', function(){
    it('should return -1 when the value is not present', function(done){
      [1,2,3].indexOf(5).should.equal(-1);
      [1,2,3].indexOf(0).should.equal(-1);
    })
  })
})

This test will not finish normally and will be timed out by Mocha after 2 seconds because although the done callback was supplied and expecting to be called, the test code does not call it.

Like Prototype, Mocha determines what to do by introspecting the function's argument list. But it does not resort to parsing out the argument names from the function source, it just uses the length property to find out the arity of the function. Which works like this

function myfunc(arg1){
}
myfunc.length // gives 1

The offending line in the Mocha source is

this.async = fn && fn.length;

So, if the length is 0, it is sync, otherwise it is async.

Should I Do This?

This is a neat trick you can employ if you are writing a JS library and want to tweak the appearance of the client code for reducing code clutter or just to make it look a certain way. It has the downside that if the people using your library is not aware of the trick, they could experience behavior that they are not expecting and don't know where to look. In my opinion, you generally should not use this. That doesn't mean don't use it, it just means: when you use it, you should have a damn good reason.

blog comments powered by Disqus