The trick to aspect oriented programming in a language like JavaScript is to use and abuse its dynamic nature. In JavaScript you can define new functions at runtime, you can wrap one function in another, and you can rename the properties of an object.
Rename
For example, let’s consider an abstraction of the gBrowser object:
gBrowser = {
addTab: function addTab() {
return "reference to new tab"
}
}
So, saying gBrowser.addTab() returns “reference to new tab”. I said that you can rename properties, and its easy:
gBrowser.newNameForAddTab = gBrowser.addTab delete gBrowser.addTab
Now gBrowser.newNameForAddTab and gBrowser.addTab point to the same function. You can even decide on the new name dynamically:
function rename(object, oldName, newName) {
object[newName] = object[oldName]
delete object[oldName]
}<br/>
// Let's return the function to its original name.
rename(gBrowser, 'newNameForAddTab', 'addTab')
Now we’re back where we started. That’s it for renaming.
Wrap
An abstraction of openedTab looks something like this:
listOfTabEvents = []<br/>
function openedTab(newTab) {
listOfTabEvents.push(newTab)
return newTab
}
If we want to pass the result of gBrowser.addTab to openedTab, we simply say:
openedTab(gBrowser.addTab())
Since JavaScript has closures, we can parameterize over addTab and openedTab:
function wrap(baseFunction, wrappingFunction) {
return function() {
return wrappingFunction(baseFunction())
}
}
<br/>firstCallAddTabThenCallOpenedTabWithTheResult = wrap(gBrowser.addTab, openedTab)
Unfortunately, this definition of wrap is a little too simplistic. What if addTab takes arguments? We aren’t threading them through. Likewise, the object associated with a method is not a property of the method. This means that the keyword this has dynamic scope in JavaScript. In other words, we have to tell a function what this should refer to every time we call it. How is this done? Well, let’s look at an example (though not a part of JavaScript, in documentation I use the ’=>’ operator to assert equality):
gBrowser.whatIsThis = function whatIsThis() {
return this
}
gBrowser.whatIsThis() => gBrowser<br/>
someOtherObject = {}
someOtherObject.whatIsThis = gBrowser.whatIsThis
someOtherObject.whatIsThis() => someOtherObject<br/>
whatIsThis = gBrowser.whatIsThis
whatIsThis() => window
The last assertion is true for web browser that use JavaScript. In general, when a function is called without using a dot, this will refer to the nearest global object in the scope chain. In the case of browsers, this happens to be a Window object which can be reference explicitly with the variable window. Now you know more about JavaScript than I bet you wanted to. Between the threading this and threading arguments, our definition of wrap only gets a little more tricky:
function wrap(baseFunction, wrappingFunction) {
return function() {
return wrappingFunction(baseFunction.apply(this, arguments))
}
}
Wrap and Rename
With wrap in one hand and rename in the other, we are prepared to face the aspect oriented monster (or at least its shadow). Here’s the technique for beating the boss. First you wrap, then you rename:
rename(gBrowser, 'addTab', wrap(gBrowser.addTab, openedTab))
Easy as pie. Unfortunately, there’s a few tricky dependencies still present in this code. In particular, we thread the result of the original gBrowser.addTab through openedTab. This means that we need to ensure that openedTab returns the same result otherwise, the new definition of gBrowser.addTab could return gibberish. On the flip side, we might want the new gBrowser.addTab to return something different than it did before. In that case, wrap and rename are a perfect fit. But what if we want to do more? What if we want to override the original definition of gBrowser.addTab entirely, call it twice, or even call it conditionally but with different arguments? And what if we want to return gBrowser.addTab to the state it was in before we started messing with it. Well, you happen to be in luck.
around wrap and rename
What we’re really looking for is something called around advice. A piece of around advice takes a special function called proceed as its argument. When proceed is called, it proceeds to call the old definition with the this and the arguments it normally would use. But what if you want to fiddle with the arguments or the value of this? For this purpose, proceed has two properties, object and args. The value of object defaults to the normal this but can be changed. Likewise, args defaults to the normal arguments array, but can be changed. Let’s look at a (contrived) example:
o = {
addSix: function addSix(x) {
return x + 6
}
}<br/>
function crazyAdvice(proceed) {
proceed.args[0] /= 2 // first we divide by two.
var y = proceed() + proceed() // call 'addSix' twice.
return y - 3
}<br/>
o.addSix(4) => 10
var aspect = around(o, 'addSix', crazyAdvice)
o.addSix(4) => 11
aspect() // call 'aspect' to uninstall.
o.addSix(4) => 10
Have around advice gives you power. Its more power than you’ll usually want. But it’s expressive enough to define more reasonable kinds of advice, like whenReturning:
function whenReturning(object, method, advice) {
return around(object, method, function aspect$whenReturning(proceed) {
var result = proceed()
advice.call(proceed.object, result)
return result
})
}
In addition to whenReturning returning, I use seven other kinds of advice: when, whenThrowing, whenCalling, after, afterReturning, afterThrowing, and before. The when kinds of advice do not affect the original call but the others can. All of their definitions are simple variations on whenReturning which you saw above. Of course, all of this begs the question of what around looks like. Since you’ve been patient enough to read this far, I’ll tell you.
How to get around
/**
* Provides around advice to a method. Around advice takes a proceed callback
* 'p' as its argument. 'p' has two properties: 'object' and
* 'args'. If called, 'p' applies the old definition of `object[method]` to
* `p.object` with arguments `p.args` and returns the result (or passes up the
* exception if one is thrown). The return value of the advice is passed
* back to the caller.
*
* @see Aspects
* @param object an object.
* @param method the name of a method in 'object'.
* @param advice a function from a proceed callback to a result value.
* @returns a function which, when invoked, removes this aspect.
*
* @author William Taysom
* @copyright Sept. 2005
* @license Use in any way just keep this notice, my name, and add yours
* if you make changes.
*/
function around(object, method, advice) {
function aspect$wrapper() {
var p = function aspect$proceed() {
return aw.jp.apply(p.object, p.args)
}
p.args = arguments
p.object = this
return advice.call(this, p)
}
var aw = aspect$wrapper
aw.jp = object[method]
aw.jp.aspect$wrapper = aw
object[method] = aw
return function aspect$uninstall() {
var awaw = aw.aspect$wrapper
var jp = aw.jp
if (awaw) {
awaw.jp = jp
jp.aspect$wrapper = awaw
} else {
object[method] = jp
delete jp.aspect$wrapper
}
}
}
Commentary