开发者

Wrapping functions and function.length

开发者 https://www.devze.com 2023-04-03 15:25 出处:网络
Let\'s consider I have the following code /*...*/ var _fun = fun; fun = function() { /*...*/ _fun.apply(this, arguments);

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.

0

精彩评论

暂无评论...
验证码 换一张
取 消

关注公众号