YakovL
YakovL

Reputation: 8316

Why this method to modify a function's arguments array fails when a param is omitted?

I'm modifying several functions of some "core" JS software using "hijacking":

var orig_func = func; // initially function(a,b,c,d)

func = function(a,b,c,d) {
    // modify arguments
    d = someCustomization(c,d);
    // do additional stuff
    doSomethingCustom(a);
    // apply the original "core" function
    return orig_func.apply(this,arguments);
}

which works nicely unless the modified params are optional: calling

func(a,b);

works in a way that d is undefined even though someCustomization returns something else when both its arguments are undefined.

Here's an MVCE for using in cosole:

var f = function(optionalParam){ if(optionalParam) console.log(optionalParam); };
var orig_f = f;
f = function(optionalParam){
    optionalParam = optionalParam ? 'custom: '+optionalParam : 'default';
    orig_f.apply(this, arguments);
};
f(); // shows undefined
f('test'); // shows 'custom: test'

"Expected" behaviour is to see "default" in console in the first case, but I get undefined instead.

After some experiments and using this thread where I've summarized adding a positioned param in a corresponding answer I've come with the following solution in terms of MCVE:

var f = function(some,optionalParam){
    console.log('some:',some);
    console.log('optionalParam:',optionalParam);
};
var orig_f = f;
f = function(some,optionalParam){
    if(optionalParam)
        optionalParam = 'custom: '+optionalParam;
    else {
        var argPosition = 2;
        while(arguments.length < argPosition)
            [].push.call(arguments,undefined);
        arguments[argPosition-1] = 'default';
    }
    orig_f.apply(this, arguments);
};
f(); // shows 'some: undefined' and 'optionalParam: default'

or in terms of the initial task:

var orig_func = func; // initially function(a,b,c,d)

func = function(a,b,c,d) {
    // modify arguments
    while(arguments.length < 4)
        [].push.call(arguments,undefined);
    arguments[3] = someCustomization(c,d);
    // do additional stuff
    doSomethingCustom(a);
    // apply the original "core" function
    return orig_func.apply(this,arguments);
}

But I'm not really able to explain what's the problem with the initial approach: why it worked for required (used) params and failed for an optional (unused) one? Does it have anything to do with closures? Why d is "not connected" with arguments in the second case?

Upvotes: 0

Views: 52

Answers (2)

YakovL
YakovL

Reputation: 8316

Right, with the help of georg ("magic bindings" keyword) and some more research I've come to an almost sufficient explanation:

  • in the old standart (haven't found which one, though) argument variables (a,b,c,d) and arguments[i] point to the same values (not sure how this is done in JS, though)

    • in the new standart this is not so (like Bergi has said, it is deprecated): changing arguments[0] won't affect a and vise versa; like georg said, it is so in the strict mode:

      function f(x) {
          "use strict";
          arguments[0] = 5;
          console.log( x );
      }
      f(1); // logs 1
      
  • now when all variables are present, both a,b,c,d (as variables scoped to the function) and arguments[1-4] are defined and connected pairwise pointing to the same values. When params c and d are omitted, arguments[3] and arguments[4] are not defined and even if we define them, there's no "connection" between them and the variables (again, I'm not sure if such connection can be created manually)

  • basic recomendation is if we need to modify arguments, we should either use only arguments (and .apply them) or only local variables (and use .call) or mix them but don't rely on the connection between them

Upvotes: 0

georg
georg

Reputation: 214949

arguments "magic bindings" are created by iterating actual arguments, so if an arg is not there, no binding will be created:

function a(x, y) {
    y = 42;
    console.log(arguments)
}

a(10, 20)
a(10)

That said, using magic bindings is a bad idea, and they won't work in the strict mode anyways. If the number of arguments is known beforehand, simply do

orig_func.call(this, a, b, c, d);

otherwise use a splat ...args and manipulate that.

Upvotes: 3

Related Questions