Wrapping functions and function.length
Asked Answered
S

4

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.

Seedman answered 6/9, 2011 at 20:43 Comment(3)
You really need lengthto be properly set? You may want have a look at developer.mozilla.org/en/JavaScript/Reference/Global_Objects/…, many libraries also simulate this behavior.Supreme
@Supreme how is bind going to fix my problem?Seedman
only a suggestion, can't understand clearly your objective, sorry.Supreme
G
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);
});
Gorgoneion answered 6/9, 2011 at 21:22 Comment(0)
J
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

Jetsam answered 7/9, 2011 at 6:6 Comment(4)
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 functionsSeedman
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.Jetsam
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.Jetsam
that's nice and all but that doesn't help make a new function with the same length as the old functionSeedman
S
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);
}
Sadiesadira answered 19/11, 2011 at 8:49 Comment(0)
F
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.

Friulian answered 15/2, 2013 at 12:30 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.