Wrapping functions and function.length
Let's consider I have the following code
/*...*/
var _fun = fun;
fun = function() {
/*...*/
_fun.apply(this, arguments);
}
I have just lost开发者_Python百科 the .length
data on _fun
because I tried to wrap it with some interception logic.
The following doesn't work
var f = function(a,b) { };
console.log(f.length); // 2
f.length = 4;
console.log(f.length); // 2
The annotated ES5.1 specification states that .length
is defined as follows
Object.defineProperty(fun, "length", {
value: /*...*/,
writable: false,
configurable: false,
enumerable: false
}
Given that the logic inside fun
requires .length
to be accurate, how can I intercept and overwrite this function without destroying the .length
data?
I have a feeling I will need to use eval
and the dodgy Function.prototype.toString
to construct a new string with the same number of arguments. I want to avoid this.
I know you'd prefer some other way, but all I can think of is to hack together something with the Function
constructor. Messy, to say the least, but it seems to work:
var replaceFn = (function(){
var args = 'abcdefghijklmnopqrstuvwxyz'.split('');
return function replaceFn(oldFn, newFn) {
var argSig = args.slice(0, oldFn.length).join(',');
return Function(
'argSig, newFn',
'return function('
+ argSig +
'){return newFn.apply(this, arguments)}'
)(argSig, newFn);
};
}());
// Usage:
var _fun = fun;
fun = replaceFn(fun, function() {
/* ... */
_fun.apply(this, arguments);
});
Faking length correctly and consistently is the final frontier in javascript and that's pretty much the beginning and end of it. In a language where you can fake just about everything, length is still somewhat magical. ES6 will deliver, and we can fake it now to greater and lesser degrees depending which engine and version you're in. For general web compatability it's a ways off. Proxies/noSuchMethod has been in Mozilla for a while. Proxies and WeakMaps have gotten to usable in V8 in Chromium and and node (requiring flags to enable) which provide the tool you need to fake length correctly.
In detail on "length": http://perfectionkills.com/how-ecmascript-5-still-does-not-allow-to-subclass-an-array/
The eventual solution: http://wiki.ecmascript.org/doku.php?id=harmony:proxies + http://wiki.ecmascript.org/doku.php?id=harmony:weak_maps
I use the following function for this purpose; it’s really fast for functions with reasonable parameter counts, more flexible than the accepted answer and works for functions with more than 26 parameters.
function fakeFunctionLength(fn, length) {
var fns = [
function () { return fn.apply(this, arguments); },
function (a) { return fn.apply(this, arguments); },
function (a, b) { return fn.apply(this, arguments); },
function (a, b, c) { return fn.apply(this, arguments); },
function (a, b, c, d) { return fn.apply(this, arguments); },
function (a, b, c, d, e) { return fn.apply(this, arguments); },
function (a, b, c, d, e, f) { return fn.apply(this, arguments); }
], argstring;
if (length < fns.length) {
return fns[length];
}
argstring = '';
while (--length) {
argstring += ',_' + length;
}
return new Function('fn',
'return function (_' + argstring + ') {' +
'return fn.apply(this, arguments);' +
'};')(fn);
}
You only need to go down the eval
/Function
route if you need to support functions with any number of parameters. If you can set a reasonable upper limit (my example is 5) then you can do the following:
var wrapFunction = function( func, code, where ){
var f;
switch ( where ) {
case 'after':
f = function(t,a,r){ r = func.apply(t,a); code.apply(t,a); return r; }
break;
case 'around':
f = function(t,a){ return code.call(t,func,a); }
break;
default:
case 'before':
f = function(t,a){ code.apply(t,a); return func.apply(t,a); }
break;
}
switch ( func.length ) {
case 0: return function(){return f(this, arguments);}; break;
case 1: return function(a){return f(this, arguments);}; break;
case 2: return function(a,b){return f(this, arguments);}; break;
case 3: return function(a,b,c){return f(this, arguments);}; break;
case 4: return function(a,b,c,d){return f(this, arguments);}; break;
case 5: return function(a,b,c,d,e){return f(this, arguments);}; break;
default:
console.warn('Too many arguments to wrap successfully.');
break;
}
}
This method of wrapping code is also extendable by creating different where
switches. I've implemented before
and after
because they are the most useful for my own project — and around
just because it reminds me of lisp. Using this set-up you could also hand off the wrapping (var f
) to external code allowing you to develop a plugin like system for where
keywords, meaning that you could easily extend or override what wrapFunction
supported.
Obviously you can change how the code is actually wrapped however you like, the key really is just using a similar technique to 999 and AdrianLang, just without worrying about building strings and passing to new Function
.
精彩评论