Sunday, April 13, 2008

Annotations and Aspects in Javascript

Here's the moment, I've been building up to:
var logAround=function(func,name) {
this[name]=defineAround(function(proceedFunc) {
print("before: "+name+" "+Array.prototype.slice.call(arguments,1));
var result=proceedFunc();
print("after: "+name+" "+result);
return result;
}, func);
}

var doesItLog=function(each) {
return each.log == true;
}

Transaction.prototype.eachFunction(logAround.onlyWhen(doesItLog));

var test=new Transaction();
test.begin();
test.commit();

Here's the output:
js> load("aspect.js")
before: begin []
begin
after: begin undefined
before: commit []
commit
after: commit undefined

Not much to explain at all. I think the code speaks for itself. The output shows the arguments passed in and what the results were from the call. I didn't return anything from either method, but it wouldn't be hard to verify that it does indeed work. I implemented my own "toString" for Arrays to print out the arguments and it's worth looking at. "arguments" is not an array, but we can call array functions on it. Most of the built-in Array functions can be called on any object. You just have to use the wordy "Array.prototype.functionName.call" to use it.

I wrote this for this blog post, but I'm planning on beefing it up with exception handling and un-weaving among other things. I also plan on adding more tests to my test suite that I did not show. The purpose was to show some cool tricks in Javascript and I hope it was fun.

And here's the entire implementation once more:
function defineBefore(beforeFunc, proceedFunc) {
return function() {
beforeFunc.apply(this,arguments);
return proceedFunc.apply(this, arguments);
};
}

function defineAfter(afterFunc, proceedFunc) {
return function() {
var result=proceedFunc.apply(this, arguments);
afterFunc.apply(this,arguments);
return result;
};
}

function defineAround(aroundFunc, proceedFunc) {
return function() {
var args=Array.prototype.slice.call(arguments,0);
args.unshift(proceedFunc.bindTo(this));
return aroundFunc.apply(this, args);
};
}

Function.prototype.bindTo=function(thisObject) {
var selfFunc=this;
return function() {
return selfFunc.apply(thisObject, arguments);
};
}

Function.prototype.everyTimeButFirst=function() {
var self=this;
var toCall=function() {
toCall=self;
return null;
};
return function() {
return toCall.apply(this, arguments);
};
}

Function.prototype.onlyWhen=function(ruleFunc) {
var self=this;
return function() {
if (ruleFunc.apply(this, arguments))
return self.apply(this, arguments);
}
}

Object.prototype.each=function(everyFunc) {
for (var name in this) if (this.hasOwnProperty(name)) {
everyFunc.call(this, this[name],name);
}
}

Object.prototype.eachFunction=function(everyFunc) {
this.each(everyFunc.onlyWhen(function(every) {
return every.constructor == Function;
}));
}

Object.prototype.run=function(func) {
func.call(this);
}

Object.prototype.define=function(name, annotations, func) {
this[name]=func;
annotations.each(function(each, name) {
func[name]=each;
});
}

Array.prototype.each=function(everyFunc) {
for (var index=0; index < this.length; index++)
everyFunc.call(this, this[index], index)
}

Array.prototype.toString=function() {
var result="[";
var addComma=function() { result = result + ", " };
addComma=addComma.everyTimeButFirst();
Array.prototype.each.call(this, function(every) {
addComma();
result = result + every;
});
return result + "]";
}

No comments: