4

I'm currently studying Secrets of the JavaScript Ninja by John Resig and I'm hoping someone can help me further understand one of the examples.

It is a function that allows method overloading on an object, each overload has it's own definition and behaviour. He blogged about it here.

The code looks like this:

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);
};

And used like this:

addMethod(obj,'funcName',function(){});
addMethod(obj,'funcName',function(a){});
addMethod(obj,'funcName',function(a,b){});

I think I understand most of how this works but you can get a better explanation than I can give from the blog post above).

However, it accesses the value of old and fn using closures, which I'm still studying.

EDIT - added jsFiddle below.

When trying to understand it, I realised that the line return fn.apply(this, arguments) could be simply return fn() with what seems to be the same result. See an example in this jsFiddle.

So, Why is it using the apply syntax if not required?

I have tried playing with the example in jsFiddle without apply and it always seems to wo

Also, what exactly is happening when we return those functions, especially in the case of:

return old.apply(this, arguments);

I really want to get a solid understanding of not just how to use this method but why it works so any insight would be greatly appreciated.

Thanks

davy
  • 4,312
  • 9
  • 43
  • 64
  • Thanks everyone for all the answers. I will study them all and accept the best answer - although, that looks as if it may be difficult looking at the high quality of the answers :) – davy Aug 08 '13 at 11:04

6 Answers6

3

So, Why is it using the apply syntax if not required?

It is actually required for the usage.

this and arguments are different for every function and are set when they're called. By using fn(), fn will be called with an empty arguments collection or no value passed for this.

.apply(this, arguments) calls fn or old and passes along the values for both from the current function.

var obj = {};

addMethod(obj, 'funcName', function (a, b) {
    console.log(this === obj);
    console.log(a, b);
    console.log(arguments[0], arguments[1]);
});

obj.funcName(2, 3);
// true
// 2, 3
// 2, 3

Also, what exactly is happening when we return those functions, especially in the case of:

return old.apply(this, arguments);

Well, the purpose of addMethod is to create a chain of functions where each knows about and can call the old function created before it.

For the example from the book, the chain is built as:

// after: addMethod(obj, 'funcName', function(){});
obj.funcName = function(){...} ──> function(){}
// after: addMethod(obj, 'funcName', function(a){});
obj.funcName = function(){...} ──────> function(a){}
               └── function(){...} ──> function(){}
// after: addMethod(obj, 'funcName', function(a,b){});
obj.funcName = function(){...} ──────────> function(a,b){}
               └── function(){...} ──────> function(a){}
                   └── function(){...} ──> function(){}
Legend:
  `└──` represents an `old` reference
  `──>` represents a `fn` reference

Each function(){...} is a unique instance created by reevaluating the same expression in a different scope/closure:

function(){
  if (fn.length == arguments.length)
     return fn.apply(this, arguments)
  else if (typeof old == 'function')
     return old.apply(this, arguments);
}

Each .apply() then follows an "arm" or "arrow" to either an old or fn and the returns allow the result to be passed back through / in reverse.

Community
  • 1
  • 1
Jonathan Lonowski
  • 112,514
  • 31
  • 189
  • 193
  • 1
    Depending on `strict mode`, `Using fn() would call without arguments or a this value` is false. `this` would be `window` or `undefined` (see [this](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/this) ) – xanatos Aug 08 '13 at 09:32
  • 1
    @xanatos It's not false, just incomplete. The value *passed* for `this` is `undefined`, though it may be replaced or *defaulted* to the global object. I was trying to focus on the passed values, though probably shouldn't have left that out. – Jonathan Lonowski Aug 08 '13 at 09:46
  • Thanks for the very detailed answer and comments. I've tried this with five overloads in jsFiddle without using apply and can call the correct function each time using fn(). This is what confused my understanding. – davy Aug 08 '13 at 11:02
  • 1
    @davy Using `fn()` will still call the expected function, but it won't pass any info about the original call to `ninja.test()` along to it. Since you're using `old.apply(...)` in the other part, those functions will pass that info along, which is why `console.log(argument)` looks right. But, try to log `a`, `b`, etc. inside the methods: http://jsfiddle.net/n89ec/. Then, try again with `.apply()`: http://jsfiddle.net/5KryU/ – Jonathan Lonowski Aug 08 '13 at 11:15
  • @Jonathan Finally the penny has dropped. Thank you. – davy Aug 08 '13 at 11:27
2

Here is a breakdown of the code.

function addMethod(object, name, fn) {

   //get the old function from the object
   var old = object[name];

   //assign a new function to the property
   object[name] = function(){

      //See if the method signatures match, if they do execute the new method
      if (fn.length == arguments.length)

        /*
         Call the function provided using apply, the first argument "this" is the object
         it sets the context for this in the function we provide, second argument is
         the arguments provided to the function.  We must return the result of the 
         function.    

        */

         return fn.apply(this, arguments)

      //If the old property on the object is a function and the new functions signature
      //did not match call the old function assigned to the property.

      else if (typeof old == 'function')

         // Same as before calling with apply, setting context and returning result

         return old.apply(this, arguments);
};

It is important to understand how apply works. It sets the context for this within a function. For example:

var myFunction = function(){
   alert(this.msg);  //this will be set by what apply passes in as the first arg
};

var obj1 = {msg: "Hello"};
var obj2 = {msg: "World"};

myFunction.apply(obj1);
myFunction.apply(obj2);

Example http://jsfiddle.net/NCaKX/

Kevin Bowersox
  • 88,138
  • 17
  • 142
  • 176
1

I think you're missing the point of .apply

I wont try to explain it :P you can find many good explanations, such as:

tl;dr

.apply allows you set the this context. .apply allows you to pass arguments as an array, allowing for a variable number of arguments.

Community
  • 1
  • 1
Halcyon
  • 54,624
  • 10
  • 83
  • 122
1

In Javascript the this variable is not set in the moment of declaration of the function, but in the moment of execution, depending on which object was used to access the function reference.

You need to use apply not only because of the aforementioned dynamic bind of this, but also because you don't know how many arguments are there before hand; so apply receives a list of arguments and passes each element of the list as an individual argument to the function.

fortran
  • 67,715
  • 23
  • 125
  • 170
1

Using .apply the context can be specified

window.name = "window";

var object = {
    name:"object"
};

function showName(){
    alert(this.name);
}

showName(); // shows window
showName.apply(object); // shows object

more here : https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/this

Diode
  • 22,062
  • 7
  • 38
  • 50
0
fn.apply(this, arguments)

calls the fn with this as the current this and arguments as parameters.

fn()

calls the fn with this equal to window or undefined (depending if you are in "non strict mode" or in "strict mode") but without parameters.

In this Mozilla.org page there is a good discussion of this (always useful)

xanatos
  • 102,557
  • 10
  • 176
  • 249