So Prototype 1.6 has finally hit the streets, and it looks to be a very nice piece of work. Though I’m no longer using Prototype at my day job (we use YUI, plus a layer of syntactic sugar that I’m in the process of writing), I’m interested to see what is possible with the JavaScript language. And, as it’s MIT-licensed, I can still borrow features from Prototype and weave them into my development toolkit.
A while back I wrote about how to intercept calls to JavaScript functions
so you can alter their behaviour. Turns out that my method was unbelievably ugly
and unnecessarily verbose: I was ignoring JavaScript’s functional capabilities
and relying too much on copying techniques from Ruby. Let’s take a look at
Prototype’s Function#wrap
:
Function.prototype.wrap = function(wrapper) {
var __method = this;
return function() {
return wrapper.apply(this,
[__method.bind(this)].concat($A(arguments)));
}
}
It’s quite simple really: it stores a reference to the function being wrapped
(__method
), and returns a new function (beginning at line 3), which I’ll refer
to as foo
. When foo
is called, it creates a copy of __method
bound to
foo
’s scope (__method.bind(this)
). It then calls wrapper
in the scope of
foo
(wrapper.apply(this ...)
), with the bound __method
inserted as the
first argument. It’s easier to see what’s going on here with some examples:
var add = function(a, b) { return a + b; };
There we have a very simple adding function with two arguments. Using wrap
, we
can overwrite this function and change its behaviour:
add = add.wrap(function(originalAdd, a, b) {
return 2 * originalAdd(a, b);
});
add
will still take two arguments, but wrap
has inserted some magic behind
the scenes to gives us a reference to originalAdd
. After this wrapping
operation, add(3, 5)
returns 16
rather than 8
. We could have done whatever
we want with originalAdd
inside our wrapper function to change the output of
add
– we could even choose not to call originalAdd
at all and disable add
for good.
Because of the way that wrap
deals with execution scope, it works with object
methods that use the this
keyword:
var whizzBang = {
name: 'Fizz buzz',
say: function(thing) {
alert(this.name + ' likes ' + thing);
}
};
whizzBang.say('apples');
// --> alerts "Fizz buzz likes apples"
We can wrap whizzBang.say
and all our uses of the this
keyword will refer to
the proper thing:
whizzBang.say = whizzBang.say.wrap(function(originalSay, stuff) {
originalSay(stuff);
if (!this.n) this.n = 0;
alert(this.name + ' has ' + this.n + ' ' + stuff);
this.n++;
});
whizzBang.say('oranges');
// --> alerts "Fizz buzz likes oranges", then "Fizz buzz has 0 oranges"
whizzBang.say('kiwis');
// --> alerts "Fizz buzz likes kiwis", then "Fizz buzz has 1 kiwis"
So this
, both in originalSay
and inside the wrapper function, refers to
whizzBang
, just as (I hope) you’d expect it to. This is really useful, and is
actually how Prototype implements method inheritance with $super
inside
Class.create
.