19

I'm trying to call a function within an object literal that I created, using the this keyword. But an error shows up saying this.doTheMove() is not a function:

window.onload = function(){

  var animBtn = document.getElementById('startAnim');

  animBtn.addEventListener('click', Animation.init, false);

}

var Animation = {
  init: function(){

     this.doTheMove(); // I'm calling the function here, but it gives an error.

  },
  doTheMove: function(){

    alert('Animation!');

  }
}

Why is there an error?

Shaoz
  • 10,573
  • 26
  • 72
  • 100

5 Answers5

16

An explanation of what's happening. Pointy's answer is good but I want to explain it more generically. A very good research on this can be found here

An event handler is just a callback. You pass it a function and an event to listen on. Interally all it will do is call that function.

Animation.init is just a getter for that function. Think of it like this:

var callback = Animation.init
animBtn.addEventListener('click', callback, false);
...
// internal browser event handler
handler() {
   // internal handler does stuff
   ...
   // Oh click event happened. Let's call that callback
   callback();
}

So all you've done is passed in

var callback = function(){
   this.doTheMove(); // I'm calling the function here, but it gives an error.
}

By default in javascript this === window. This will refer to the global object if it isn't set to something. The net effect is that window.doTheMove is called. And that function doesn't exist.

In this case since callback is actaully called by an event handler the this object points at the DOM object that triggered the event so your calling node.doTheMove which still doesn't exist.

What you wanted to do is wrap it with a reference to Animation.

var callback = function() {
    Animation.init();
}

This is a function execution and it executes init on Animation. When you execute it on an object like that then internally this === Animation as you would expect.

To sum up. The issue here is that Animation.init is just a reference to a function. It has no information about anything else like Pointy mentioned.

Raynos
  • 166,823
  • 56
  • 351
  • 396
  • 3
    Your answer is mostly correct (absolutely correct in the end result). Only mistake is *"The net effect is that window.doTheMove is called."*. Because `init` is passed to `addEventListener`, it is actually being called from the context of the `animBtn` element so `this.doTheMove()` is effectively `animBtn.doTheMove()`. Again, the end result is the same. Just a minor detail. – user113716 Jan 27 '11 at 18:53
  • @patrickdw Thanks I wasn't sure whether `this` was passed on as the DOM object or the Window object. I didn't bother to test it to confirm. I expected as much. – Raynos Jan 27 '11 at 18:58
  • 1
    +1 And to be fair, your assessment of `this` would have been correct if it was Microsoft's `attachEvent` being used since it does not (for some reason) set the context of the handler. – user113716 Jan 27 '11 at 19:03
  • @patrickdw Ah! I forgot that. Didn't even know referencing `this` would break in `attachEvent`. I rely on jQuery handling events uniformly in a cross-browser manner too much. – Raynos Jan 27 '11 at 19:06
10

You have to change the way you set that up:

window.onload = function(){

  var animBtn = document.getElementById('startAnim');

  animBtn.addEventListener('click', function() { Animation.init(); }, false);

}

In JavaScript, the fact that a function happens to be defined as part of an object literal really doesn't mean very much (if anything, in fact). The reference to Animation.init does get you to the proper function, but the problem is that when the function is later invoked (in response to an actual "click"), the browser calls the function but has no idea that the object "Animation" should be the this reference. Again, the fact that the function was declared as part of the object is of no importance at all here. Therefore, if you want this to be something in particular of your own choosing, then you have to make sure it's set explicitly in code you control. The solution above is about the simplest way to do it: it handles the "click" events with an anonymous function that does nothing other than invoke the "init" function via an explicit reference through "Animation". That will ensure that this refers to the "Animation" object when "init" runs.

Another alternative would be to use the ".bind()" facility that some browsers and frameworks support:

window.onload = function(){

  var animBtn = document.getElementById('startAnim');

  animBtn.addEventListener('click', Animation.init.bind(Animation); }, false);

}

The net effect is almost exactly the same: that call to ".bind()" returns a function that invokes the function on which it was called (that being the "init" function in the "Animation" object), and does so with its first argument as the this reference (the "context" object). That's the same thing that we get from the first example, or effectively the same anyway.

Pointy
  • 405,095
  • 59
  • 585
  • 614
  • explain in more detail how passing the Animation.init function pointer and calling it invokes it with a `this` object that is not `Animation`. The solution is good, but the explanation why it fails in the first place is lacking. – Raynos Jan 27 '11 at 18:29
  • @Raynos yes you're right, I'll add some text to describe that. – Pointy Jan 27 '11 at 18:31
1

Here's another nice approach, I think.

window.onload = function(){

  var animBtn = document.getElementById('startAnim');

  animBtn.addEventListener('click', Animation.init, false);

};

var Animation = {
    init: function(){

        Animation.doTheMove(); // This will work, but your IDE may complain...

    },
    doTheMove: function(){

        alert('Animation!');

    }
};
StephenKC
  • 613
  • 1
  • 6
  • 6
0

Six and a half years later, but I'm hoping my answer can also provide some insight for current and future developers.

I tend to code using literal objects inside of self defined functions, and the original question posted works just fine if another self-executing function is added along with a try and catch statement.

It's very important to point out that it's all about scope and context.

Please correct any drawbacks or provide more effective suggestions of using this method.

(function() {

console.log(this);  // window object

    var animation = {
        init: function() {
            this.doTheMove();
        },
        doTheMove: function() {
            alert("Animation");
            console.log(animation); // animation object
        }
    };

    (function() {
        try {
            console.log("animation.init"); // animation.init function
            animation.init();
        } catch(e) {
            console.log("Error is: " + e);
        }
    })();

})();
SC87
  • 1
  • 3
0

You might want to use the portotype base approach:

// generate a prototype object which can be instantiated
var Animation = function() { this.doTheMove(); }
Animation.prototype.doTheMove = function() {
    // if the object has only one method, the whole code could be moved to 
    // var Animation = function() {...} above
    alert('Animation!'); 
}

Animation.prototype.otherMethod = function(param1, param2) {
  // ...
}


// run the code onload
window.onload = function(){
  var animBtn = document.getElementById('startAnim');
  animBtn.addEventListener('click', new Animation(), false);
}
Spliffster
  • 6,959
  • 2
  • 24
  • 19
  • 3
    This isn't correct. The `addEventListener` needs to receive a function. You're passing an object that references a function. – user113716 Jan 27 '11 at 18:27