4

I'm reading Secrets of Javascript Ninja and came across an example that I cannot fully understand. This same example was cited by some other user here before, but their doubts are different than mine. Here is the example:

function addMethod(object, name, fn) {

  var old = object[name];

  object[name] = function(){

       if (fn.length == arguments.length)

          return fn.apply(this, arguments)

       else if (typeof old == 'function')

          return old.apply(this, arguments);

    };
}

This is an example of function overloading, used this way:

var ninja = {};
addMethod(ninja,'whatever',function(){ /* do something */ });
addMethod(ninja,'whatever',function(a){ /* do something else */ });
addMethod(ninja,'whatever',function(a,b){ /* yet something else */ });

I have a solid understanding of scope, closure and the use of apply(). My doubts are:

  • fn.length will return the number of parameters defined in fn. Will arguments.length return the number of which arguments? The already existing function's?
  • But if so, and they match, why will it then apply the NEW function and not the existing one?
  • If arguments.length returns the number of the given function, then when will they be different?
  • I add 10 methods, starting with no parameters and increasing the number of them each time, after adding the 10th method, I call the method with no parameters, where did it 'store' the first function?
  • I don't understand the use of old here.

I probably do not have some key concept that responds to all of the questions. Feel free to give me an explanation rather than responding the questions individually.

2
  • 2
    1. arguments.length will be the # of args passed to the closest outer function from where it's called. 2. it will apply the one with the same call signature as the method(s) you added, or else go to the last one added. 3. arguments.length returns what's passed, not what's defined. 4. as old, which every methods uses closure to see. basically, it will start at the one for 10 args, and if there's not 10, then try 9, and if not 9, then try 8, all the way back to the one whose formal params matches the arguments. Commented Jun 22, 2015 at 20:55
  • 1
    incidentally, this is a very slow and ram-hungry way to setup variadic methods. if at all possible, the arity branches should all be defined in one function, using something like switch(arguments.length) to handle the routing. Commented Jun 22, 2015 at 21:12

5 Answers 5

2

fn.length will return the number of parameters defined in fn. Will arguments.length return the number of which arguments? The already existing function's?

No. arguments is an array-like local variable that's available inside of a function. It contains the number of arguments passed to the function.

The rest of your questions can answered by addressing this question:

I add 10 methods, starting with no parameters and increasing the number of them each time, after adding the 10th method, I call the method with no parameters, where did it 'store' the first function?

Looking at the addMethod method one step at a time might be helpful:

function addMethod(object, name, fn) {

  var old = object[name];
  // Get the old function corresponding to this name. Will be "undefined"
  // the first time "addMethod" is called.


  object[name] = function(){
  // Now, assign object[name] to a new function.
  // The critical part of this function is that "old" is captured inside of
  // this function and will be available any time the function is called.

       if (fn.length == arguments.length)
       // if the number of parameters belonging to the function we've added
       // matches what was passed in, call "fn"
          return fn.apply(this, arguments)

       else if (typeof old == 'function')
       // Otherwise if there's another function with this name
       // call it instead.
          return old.apply(this, arguments);

    };
}

So lets take each call to addMethod in your example and examine the values of fn and old. You can think of the way that this is organized as functions stacked on top of each other, leveraging the scope of old.

addMethod(ninja,'whatever',function(){ /* do something */ });
// old === undefined

addMethod(ninja,'whatever',function(a){ /* do something else */ });
// old === function #1

addMethod(ninja,'whatever',function(a,b){ /* yet something else */ });
// old === function #2

At the end of calling addMethod three times ninja.whatever refers to a function that calls function #3 if possible, and then calls the old function (function #2) if the arguments don't match function #3's parameter list length.

So visually, you can think of it like this:

function #3 (a,b)
function #2 (a)
function #1 ()

Where each function's old reference points to the function underneath it.

Now let's examine what happens when you call ninja.whatever(). This function is at the "bottom" of our stack.

  1. The function currently associated with whatever is function #3. When calling function #3, fn.length != arguments.length, and so old (function #2) is executed.
  2. So now we're executing function #2. Again, when calling function #2, fn.length != arguments.length, and old (function #1) is executed.
  3. Finally, function #1 is executed. This time fn.length == arguments.length and so the function is called.
Sign up to request clarification or add additional context in comments.

1 Comment

It gets more clear now. The cascade-closure scheme is the key concept I was missing. Thank you for the detailed question it was really helpful.
1

Comments added:

//replace object[name] with a wrapper that either calls the passed-in 
//function (fn) or the old value of object[name]
function addMethod(object, name, fn) {

  //store object[name]    
  var old = object[name];



  object[name] = function(){

        //if the wrapper is called with as many arguments as is the arity of the passed in function (fn), call the passed in function (fn)
       if (fn.length == arguments.length)

          return fn.apply(this, arguments)

       //otherwise call the old value of object[name] but only if it is a function
       else if (typeof old == 'function')

          return old.apply(this, arguments);

    };
}

Comments

1

When a function is created it retains access to variables in the scope where it's defined. In this case every method we create has access a scope with variables old, object, name, and fn. The old variable is accessible from each method we add and the reference to it from the main object is overwritten so it's only accessible from "old" in that scope. You basically have a bunch of "old" variables are cascade through them to find the one that handles the number of arguments given.

function addMethod(object, name, fn) {

  // Get a reference to the existing method name.
  var old = object[name];

  /*
    Write over the method name with a new method
    that checks for a specific argument length.
    If the method is called with a different
    argument length and "old" exists call the
    old method.
  */

  object[name] = function(){

       if (fn.length == arguments.length)

          return fn.apply(this, arguments)

       else if (typeof old == 'function')

          return old.apply(this, arguments);

    };
}

Comments

1

function.length is the number of arguments a function expects, the formal parameters. arguments.length is the actual number of arguments passed to the function, and that means the already existing function, because no arguments could have been passed to the new function; it wasn't called and is just a value. The arguments object is an array-like object containing the passed arguments. (the length property might make more sense knowing this). The difference between the length of given arguments and formal parameters is that a function can be passed more or less arguments than stated parameters. console.log() accepts any number of arguments, for example. Some of these functions just wrap a loop around the arguments object and to do something with each element in the arguments object. Each method added is stored as a property of the object given as the first parameter to the addMethod function. The variable old is just a variable, and names are not necessary to understand concepts. Perhaps the author was implying the mutability of objects and their properties by old (mutability is the ability to give a property a new value/ modify the object after its declaration).

Comments

0

Most of your first questions are addressed by dandavis' comments. arguments.length gives the number of arguments applied to the function declared in the line

object[name] = function(){

The method is 'stored' in the ninja object declared in your first line of code and passed into subsequent calls.

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.