prototype/src/lang/function.js

282 lines
9.8 KiB
JavaScript
Raw Normal View History

2009-03-05 19:56:01 +00:00
/** section: Language
* class Function
*
2009-03-20 10:46:53 +00:00
* Extensions to the built-in `Function` object.
2008-12-14 04:36:59 +00:00
**/
2008-12-11 10:49:41 +00:00
Object.extend(Function.prototype, (function() {
var slice = Array.prototype.slice;
2008-12-11 10:49:41 +00:00
function update(array, args) {
var arrayLength = array.length, length = args.length;
while (length--) array[arrayLength + length] = args[length];
return array;
}
2008-12-11 10:49:41 +00:00
function merge(array, args) {
array = slice.call(array, 0);
return update(array, args);
}
2008-12-14 04:36:59 +00:00
/**
* Function#argumentNames() -> Array
*
* Reads the argument names as stated in the function definition and returns
* the values as an array of strings (or an empty array if the function is
* defined without parameters).
*
* ### Examples
*
* function fn(foo, bar) {
* return foo + bar;
* }
* fn.argumentNames();
* //-> ['foo', 'bar']
*
* Prototype.emptyFunction.argumentNames();
* //-> []
2008-12-14 04:36:59 +00:00
**/
2008-12-11 10:49:41 +00:00
function argumentNames() {
var names = this.toString().match(/^[\s\(]*function[^(]*\(([^)]*)\)/)[1]
.replace(/\/\/.*?[\r\n]|\/\*(?:.|[\r\n])*?\*\//g, '')
2008-12-11 10:42:15 +00:00
.replace(/\s+/g, '').split(',');
return names.length == 1 && !names[0] ? [] : names;
2008-12-11 10:49:41 +00:00
}
/** related to: Function#bindAsEventListener
* Function#bind(context[, args...]) -> Function
* - context (Object): The object to bind to.
* - args (?): Optional additional arguments to curry for the function.
*
* Binds this function to the given `context` by wrapping it in another
* function and returning the wrapper. Whenever the resulting "bound"
* function is called, it will call the original ensuring that `this` is set
* to `context`. Also optionally curries arguments for the function.
*
* ### Examples
*
* A typical use of `Function#bind` is to ensure that a callback (event
* handler, etc.) that is an object method gets called with the correct
* object as its context (`this` value):
*
* var AlertOnClick = Class.create({
* initialize: function(msg) {
* this.msg = msg;
* },
* handleClick: function(event) {
* event.stop();
* alert(this.msg);
* }
* });
* var myalert = new AlertOnClick("Clicked!");
* $('foo').observe('click', myalert.handleClick); // <= WRONG
* // -> If 'foo' is clicked, the alert will be blank; "this" is wrong
* $('bar').observe('click', myalert.handleClick.bind(myalert)); // <= RIGHT
* // -> If 'bar' is clicked, the alert will be "Clicked!"
*
* `bind` can also *curry* (burn in) arguments for the function if you
* provide them after the `context` argument:
*
* var Averager = Class.create({
* initialize: function() {
* this.count = 0;
* this.total = 0;
* },
* add: function(addend) {
* ++this.count;
* this.total += addend;
* },
* getAverage: function() {
* return this.count == 0 ? NaN : this.total / this.count;
* }
* });
* var a = new Averager();
* var b = new Averager();
* var aAdd5 = a.add.bind(a, 5); // Bind to a, curry 5
* var aAdd10 = a.add.bind(a, 10); // Bind to a, curry 10
* var bAdd20 = b.add.bind(b, 20); // Bind to b, curry 20
* aAdd5();
* aAdd10();
* bAdd20();
* bAdd20();
* alert(a.getAverage());
* // -> Alerts "7.5" (average of [5, 10])
* alert(b.getAverage());
* // -> Alerts "20" (average of [20, 20])
2008-12-14 04:36:59 +00:00
*
* (To curry without binding, see [[Function#curry]].)
2008-12-14 04:36:59 +00:00
**/
2008-12-11 10:49:41 +00:00
function bind(context) {
2008-12-11 10:42:15 +00:00
if (arguments.length < 2 && Object.isUndefined(arguments[0])) return this;
2008-12-11 10:49:41 +00:00
var __method = this, args = slice.call(arguments, 1);
2008-12-11 10:42:15 +00:00
return function() {
2008-12-11 10:49:41 +00:00
var a = merge(args, arguments);
return __method.apply(context, a);
2008-12-11 10:42:15 +00:00
}
2008-12-11 10:49:41 +00:00
}
2008-12-14 04:36:59 +00:00
/** related to: Function#bind
* Function#bindAsEventListener(context[, args...]) -> Function
* - context (Object): The object to bind to.
* - args (?): Optional arguments to curry after the event argument.
2008-12-14 04:36:59 +00:00
*
* An event-specific variant of [[Function#bind]] which ensures the function
* will recieve the current event object as the first argument when
* executing.
*
* It is not necessary to use `bindAsEventListener` for all bound event
* handlers; [[Function#bind]] works well for the vast majority of cases.
* `bindAsEventListener` is only needed when:
*
* - Using old-style DOM0 handlers rather than handlers hooked up via
* [[Event.observe]], because `bindAsEventListener` gets the event object
* from the right place (even on MSIE). (If you're using `Event.observe`,
* that's already handled.)
* - You want to bind an event handler and curry additional arguments but
* have those arguments appear after, rather than before, the event object.
* This mostly happens if the number of arguments will vary, and so you
* want to know the event object is the first argument.
*
* ### Example
*
* var ContentUpdater = Class.create({
* initialize: function(initialData) {
* this.data = Object.extend({}, initialData);
* },
* // On an event, update the content in the elements whose
* // IDs are passed as arguments from our data
* updateTheseHandler: function(event) {
* var argIndex, id, element;
* event.stop();
* for (argIndex = 1; argIndex < arguments.length; ++argIndex) {
* id = arguments[argIndex];
* element = $(id);
* if (element) {
* element.update(String(this.data[id]).escapeHTML());
* }
* }
* }
* });
* var cu = new ContentUpdater({
* dispName: 'Joe Bloggs',
* dispTitle: 'Manager <provisional>',
* dispAge: 47
* });
* // Using bindAsEventListener because of the variable arg lists:
* $('btnUpdateName').observe('click',
* cu.updateTheseHandler.bindAsEventListener(cu, 'dispName')
* );
* $('btnUpdateAll').observe('click',
* cu.updateTheseHandler.bindAsEventListener(cu, 'dispName', 'dispTitle', 'dispAge')
* );
2008-12-14 04:36:59 +00:00
**/
2008-12-11 10:49:41 +00:00
function bindAsEventListener(context) {
var __method = this, args = slice.call(arguments, 1);
2008-12-11 10:42:15 +00:00
return function(event) {
2008-12-11 10:49:41 +00:00
var a = update([event || window.event], args);
return __method.apply(context, a);
2008-12-11 10:42:15 +00:00
}
2008-12-11 10:49:41 +00:00
}
2008-12-14 04:36:59 +00:00
/**
* Function#curry(args...) -> Function
* Partially applies the function, returning a function with one or more
* arguments already "filled in."
2008-12-14 04:36:59 +00:00
*
* Function#curry works just like [[Function#bind]] without the initial
* scope argument. Use the latter if you need to partially apply a function
* _and_ modify its execution scope at the same time.
**/
2008-12-11 10:49:41 +00:00
function curry() {
2008-12-11 10:42:15 +00:00
if (!arguments.length) return this;
2008-12-11 10:49:41 +00:00
var __method = this, args = slice.call(arguments, 0);
2008-12-11 10:42:15 +00:00
return function() {
2008-12-11 10:49:41 +00:00
var a = merge(args, arguments);
return __method.apply(this, a);
2008-12-11 10:42:15 +00:00
}
2008-12-11 10:49:41 +00:00
}
2008-12-11 10:42:15 +00:00
2008-12-14 04:36:59 +00:00
/**
* Function#delay(seconds[, args...]) -> Number
* - seconds (Number): How long to wait before calling the function.
*
* Schedules the function to run after the specified amount of time, passing
* any arguments given.
*
* Behaves much like `window.setTimeout`. Returns an integer ID that can be
* used to clear the timeout with `window.clearTimeout` before it runs.
*
* To schedule a function to run as soon as the interpreter is idle, use
* [[Function#defer]].
**/
function delay(timeout) {
2008-12-11 10:49:41 +00:00
var __method = this, args = slice.call(arguments, 1);
timeout = timeout * 1000
2008-12-11 10:42:15 +00:00
return window.setTimeout(function() {
return __method.apply(__method, args);
}, timeout);
2008-12-11 10:49:41 +00:00
}
2008-12-14 04:36:59 +00:00
/**
* Function#defer(args...) -> Number
* Schedules the function to run as soon as the interpreter is idle.
*
* A "deferred" function will not run immediately; rather, it will run as soon
* as the interpreter's call stack is empty.
2008-12-14 04:36:59 +00:00
*
* Behaves much like `window.setTimeout` with a delay set to `0`. Returns an
* ID that can be used to clear the timeout with `window.clearTimeout` before
* it runs.
**/
2008-12-11 10:49:41 +00:00
function defer() {
var args = update([0.01], arguments);
2008-12-11 10:42:15 +00:00
return this.delay.apply(this, args);
2008-12-11 10:49:41 +00:00
}
2008-12-14 04:36:59 +00:00
/**
* Function#wrap(wrapperFunction) -> Function
* - wrapperFunction (Function): The function to act as a wrapper.
*
* Returns a function "wrapped" around the original function.
2008-12-14 04:36:59 +00:00
*
* `Function#wrap` distills the essence of aspect-oriented programming into
* a single method, letting you easily build on existing functions by
* specifying before and after behavior, transforming the return value, or
* even preventing the original function from being called.
**/
2008-12-11 10:49:41 +00:00
function wrap(wrapper) {
2008-12-11 10:42:15 +00:00
var __method = this;
return function() {
2008-12-11 10:49:41 +00:00
var a = update([__method.bind(this)], arguments);
return wrapper.apply(this, a);
2008-12-11 10:42:15 +00:00
}
2008-12-11 10:49:41 +00:00
}
2008-12-14 04:36:59 +00:00
/**
* Function#methodize() -> Function
* Wraps the function inside another function that, at call time, pushes
* `this` to the original function as the first argument.
*
* Used to define both a generic method and an instance method.
**/
2008-12-11 10:49:41 +00:00
function methodize() {
2008-12-11 10:42:15 +00:00
if (this._methodized) return this._methodized;
var __method = this;
return this._methodized = function() {
2008-12-11 10:49:41 +00:00
var a = update([this], arguments);
return __method.apply(null, a);
2008-12-11 10:42:15 +00:00
};
}
2008-12-11 10:49:41 +00:00
return {
argumentNames: argumentNames,
bind: bind,
bindAsEventListener: bindAsEventListener,
curry: curry,
delay: delay,
defer: defer,
wrap: wrap,
methodize: methodize
}
})());