3 * http://github.com/cowboy/javascript-hooker
5 * Copyright (c) 2012 "Cowboy" Ben Alman
6 * Licensed under the MIT license.
7 * http://benalman.com/about/license/
11 // Get an array from an array-like object with slice.call(arrayLikeObject).
13 // Get an "[object [[Class]]]" string with toString.call(value).
14 var toString = {}.toString;
16 // I can't think of a better way to ensure a value is a specific type other
17 // than to create instances and use the `instanceof` operator.
18 function HookerOverride(v) { this.value = v; }
19 function HookerPreempt(v) { this.value = v; }
20 function HookerFilter(c, a) { this.context = c; this.args = a; }
22 // When a pre- or post-hook returns the result of this function, the value
23 // passed will be used in place of the original function's return value. Any
24 // post-hook override value will take precedence over a pre-hook override
26 exports.override = function(value) {
27 return new HookerOverride(value);
30 // When a pre-hook returns the result of this function, the value passed will
31 // be used in place of the original function's return value, and the original
32 // function will NOT be executed.
33 exports.preempt = function(value) {
34 return new HookerPreempt(value);
37 // When a pre-hook returns the result of this function, the context and
38 // arguments passed will be applied into the original function.
39 exports.filter = function(context, args) {
40 return new HookerFilter(context, args);
43 // Execute callback(s) for properties of the specified object.
44 function forMethods(obj, props, callback) {
46 if (typeof props === "string") {
47 // A single prop string was passed. Create an array.
49 } else if (props == null) {
50 // No props were passed, so iterate over all properties, building an
51 // array. Unfortunately, Object.keys(obj) doesn't work everywhere yet, so
52 // this has to be done manually.
55 if (obj.hasOwnProperty(prop)) {
60 // Execute callback for every method in the props array.
63 // If the property isn't a function...
64 if (toString.call(obj[props[i]]) !== "[object Function]" ||
65 // ...or the callback returns false...
66 callback(obj, props[i]) === false) {
67 // ...remove it from the props array to be returned.
71 // Return an array of method names for which the callback didn't fail.
75 // Monkey-patch (hook) a method of an object.
76 exports.hook = function(obj, props, options) {
77 // If the props argument was omitted, shuffle the arguments.
78 if (options == null) {
82 // If just a function is passed instead of an options hash, use that as a
84 if (typeof options === "function") {
85 options = {pre: options};
88 // Hook the specified method of the object.
89 return forMethods(obj, props, function(obj, prop) {
90 // The original (current) method.
92 // The new hooked function.
94 var result, origResult, tmp;
96 // Get an array of arguments.
97 var args = slice.call(arguments);
99 // If passName option is specified, prepend prop to the args array,
100 // passing it as the first argument to any specified hook functions.
101 if (options.passName) {
105 // If a pre-hook function was specified, invoke it in the current
106 // context with the passed-in arguments, and store its result.
108 result = options.pre.apply(this, args);
111 if (result instanceof HookerFilter) {
112 // If the pre-hook returned hooker.filter(context, args), invoke the
113 // original function with that context and arguments, and store its
115 origResult = result = orig.apply(result.context, result.args);
116 } else if (result instanceof HookerPreempt) {
117 // If the pre-hook returned hooker.preempt(value) just use the passed
118 // value and don't execute the original function.
119 origResult = result = result.value;
121 // Invoke the original function in the current context with the
122 // passed-in arguments, and store its result.
123 origResult = orig.apply(this, arguments);
124 // If the pre-hook returned hooker.override(value), use the passed
125 // value, otherwise use the original function's result.
126 result = result instanceof HookerOverride ? result.value : origResult;
130 // If a post-hook function was specified, invoke it in the current
131 // context, passing in the result of the original function as the
132 // first argument, followed by any passed-in arguments.
133 tmp = options.post.apply(this, [origResult].concat(args));
134 if (tmp instanceof HookerOverride) {
135 // If the post-hook returned hooker.override(value), use the passed
136 // value, otherwise use the previously computed result.
141 // Unhook if the "once" option was specified.
143 exports.unhook(obj, prop);
146 // Return the result!
149 // Re-define the method.
151 // Fail if the function couldn't be hooked.
152 if (obj[prop] !== hooked) { return false; }
153 // Store a reference to the original method as a property on the new one.
154 obj[prop]._orig = orig;
158 // Get a reference to the original method from a hooked function.
159 exports.orig = function(obj, prop) {
160 return obj[prop]._orig;
163 // Un-monkey-patch (unhook) a method of an object.
164 exports.unhook = function(obj, props) {
165 return forMethods(obj, props, function(obj, prop) {
166 // Get a reference to the original method, if it exists.
167 var orig = exports.orig(obj, prop);
168 // If there's no original method, it can't be unhooked, so fail.
169 if (!orig) { return false; }
170 // Unhook the method.
174 }(typeof exports === "object" && exports || this));