Protolicious
Prototype.js provides a rich set of solid components for building your web applications. Since most of those components are relatively low-level, prototype allows for an easy extension of basic functionality. For the past couple of months, I’ve been using few helpers that make painful tasks less painful. These helpers/extensions are published under the name of Protolicious (MIT license). Today, we’ll go over one of the modules from the suit – Function Extensions.
Function#not
Definition:
Function.prototype.not = function() {
var f = this;
return function() {
return !f.apply(f, arguments);
}
};
This is a pretty common pattern of “inverting” return value of a function. not
acts as a function “factory” – returning a function which returns “inverted” result of original one. It’s especially useful when working with iterators, since “!” operator does not invert return value but acts immediately – inverting function’s textual representation. Example:
// Find all hidden elements (style.display == 'none')
// The usual way
$$('*').findAll(function(element) { return !element.visible() });
// Using Function#not
$$('*').findAll(Element.visible.not());
Function#runOnce
Definition:
Function.prototype.runOnce = function() {
this.apply(this, arguments);
return this;
};
runOnce
is useful when working with event handlers. All it does is execute a function and return it. Sometimes certain behavior should not only be attached to elements, but also executed at the time it was initialized. It might seem trivial to wrap a function with “(” and “)” – forcing an expression to be evaluated – but the return value is then the one of a function, not the function itself. Another way is to call a handler before or after it has been attached. That works, unless a handler is an anonymous function. Otherwise, it needs to be named. Besides these minor pet peeves, I find runOnce
looking much cleaner: Example:
// the usual way
myElement.toggle(); // run handler right away
input.observe('change', myElement.toggle); // and then attach it
// new way
input.observe('change', myElement.toggle.runOnce()); // does same thing as above
Function#toDelayed & Function#toDeferred
Definition:
Function.prototype.toDelayed = function(timeout) {
var __method = this;
return function() {
var args = $A(arguments);
setTimeout(function(){ __method.apply(__method, args) }, timeout * 1000);
}
};
Function.prototype.toDeferred = function() {
return this.toDelayed(0.01);
};
Prototype 1.6 introduced 2 wonderful function methods – Function#delay and Function#defer. These wrappers are essentially just a syntactic sugar on top of window.setTimeout
. They allow to invoke function in a specified period of time. When working with event handlers or “heavy” applications, delayed execution appears to be quite essential. I noticed that when observing certain form events (e.g. “change” or “reset”), observer function needs be deferred in order for a form element to catch up with the new changes. toDelayed
/toDeferred
are another pair of function factories which simply produce delayed/deferred representation of functions they were invoked on. Example:
// handler works as expected when observing "click" event
this.radioElements.invoke('observe', 'click', handler);
// form's changes (resetting to an initial state) are not yet in affect by the time handler is called.
// toDeferred solves this problem by creating a deferred function -
// the one that will execute as soon as JS interpeter becomes available (and form's state changes)
this.formElement.observe('reset', handler.toDeferred());
Function.K
Definition:
Function.K = function(k) {
return function() {
return k;
}
};
Function.K
differs from other helpers in a way that it’s a static method on a Function object. Its goal is to accept an expression and return a function which returns that same expression. It could be used for currying, creating generic methods or encapsulating variables in a closure. Shared closure variables are a real show-stopper for unexperienced javascripters. Let’s see what the problem is all about and how Function.K
could help us: Example:
var methods = {
foo: function() { return 'foo' },
bar: function() { return 'bar' }
};
var uppercased = { };
for (var name in methods) {
// "name" variable is shared among all functions that are assigned in a loop
// all methods in "uppercased" object return uppercased value from the last iteration of the loop
// that is usually an undesired behavior
uppercased[name] = function() { return name.toUpperCase() };
};
uppercased.foo(); // 'BAR' (unexpected behavior)
for (var name in methods) {
// Function.K "encloses" "name" variable into its own closure,
// breaking undesired reference "sharing" and resulting in a "correct" behavior
uppercased[name] = Function.K(name.toUpperCase());
}
uppercased.foo(); // 'FOO' (now works as expected)
// an alternative workaround would be to explicitly wrap function and enclose variable:
// that could certainly work, but looks a little cumbersome
for (var name in methods) {
uppercased[name] = (function(name){ return function(){ return name.toUpperCase() } })(name);
}
Besides “fixing” closures, Function.K
could be used to create generic helpers:
// creates a function which always returns "true"
Prototype.trueFunction = Function.K(true);
// creates a function which always returns "false"
Prototype.falseFunction = Function.K(false);
Function.prototype.addAdvice
Definition:
Function.prototype.addAdvice = function(advices) {
return this.wrap(function() {
var args = $A(arguments), proceed = args.shift();
var a = advices, bf = a.before, ar = a.around, af = a.after;
bf && bf.apply(proceed, args);
ar && ar.apply(proceed, args);
var result = proceed.apply(proceed, args);
ar && ar.apply(proceed, result);
af && af.apply(proceed, result);
return result;
});
};
addAdvice
method somewhat simulates “before”, “after” and “around” advices from Aspect Oriented Programming. I find it useful mostly for logging purposes, but the scope of possible implementations is huge – profiling, error logging, augmenting functions with additional behavior based on certain conditions, and many many more. Here’s a simple example of augmenting function with “before” and “after” logging advices. Example:
function sum(){
return $A(arguments).inject(0, function(result, value){ return result + value; });
}
sum = sum.addAdvice({
// before callback receives arguments which original function is invoked with
before: function() { console.log('receiving: ' + $A(arguments)) },
// after callback receives result value of a function
after: function() { console.log('returning: ' + $A(arguments)) }
});
sum(1,2,3);
// logs: "receiving: 1,2,3"
// logs: "returning: 6"
There’s a test suite for these helpers – if you find any of the tests failing, don’t hesitate to let me know. I hope some of you find these helpers as useful as I find them. Have fun prototyping.
Did you like this? Donations are welcome
comments powered by Disqus