6

Let's consider I have the following code

/*...*/
var _fun = fun;
fun = function() {
  /*...*/
  _fun.apply(this, arguments);
}

I have just lost 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.

Raynos
  • 166,823
  • 56
  • 351
  • 396
  • You really need `length`to be properly set? You may want have a look at https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Function/bind, many libraries also simulate this behavior. – Prusse Sep 06 '11 at 21:08
  • @Prusse how is bind going to fix my problem? – Raynos Sep 06 '11 at 21:21
  • only a suggestion, can't understand clearly your objective, sorry. – Prusse Sep 06 '11 at 21:31

4 Answers4

3

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);
});
James
  • 109,676
  • 31
  • 162
  • 175
2

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

  • Could you explain how proxy can help fake length. And how could weak maps possibly help? Also you reference for "length" refers to arrays not to functions – Raynos Sep 07 '11 at 10:05
  • With a proxy you can make a catchall getter for indexes and replicate all the required behavior to match what the native length property does. WeakMaps are what you'd utilize to provide accessors for non-existent properties that the proxy snags before type/undefined checks are done. When something to access the getter for 401042 on your fake array you first define that property using WeakMap and then change the value of your fake length property. All native length properties are internal pointers to an actual data structure outside of JavaScripts realm and thus can show "magical" behavior. –  Sep 07 '11 at 13:40
  • Changing length on functions is seemingly less useful than Arrays but it's still magically imbued, and in this case it just behaves unpredictably between browsers. Mozilla changes the size and apparently nothing else I've seen. V8 returns the number you specify when you set it, but doesn't actually change the value and will return the actual number of parameters no matter what you do. Length is supposed to be a direct mapping to the memory holding whatever it represents. –  Sep 07 '11 at 13:43
  • that's nice and all but that doesn't help make a new function with the same length as the old function – Raynos Sep 07 '11 at 14:02
2

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);
}
Adrian Heine
  • 4,051
  • 2
  • 30
  • 43
0

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.

Pebbl
  • 34,937
  • 6
  • 62
  • 64