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.