setTimeout(this.doAnimation, 1000) will fail if it is defined in a class definition

Discussion in 'JavaScript' started by winterheat, Oct 4, 2008.

  1. #1
    I did some animation using

    var oDiv = {};
    oDiv.x = 100;
    oDiv.y = 100;
    oDiv.node = document.getElementById('divImage');
    oDiv.node.style.position = "absolute";
    
    oDiv.doAnimation = function() {
        oDiv.x += 1;
        oDiv.y += 1;
        oDiv.node.style.top = oDiv.y + 'px';
        oDiv.node.style.left = oDiv.x + 'px';
        setTimeout(oDiv.doAnimation, 33);
    }
    Code (markup):
    and it works fine. When it is changed to the traditional class definition, then it fails:

    function Animation() {
        this.x = ...
        etc...
    
        this.doAnimation = function() {
            // do something
    
            // set delay
            setTimeout(this.doAnimation, 33);
        }
    }
    
    oDiv = new Animation();
    oDiv.doAnimation();
    Code (markup):
    which will fail. The second time when the doAnimation() is called, it seems that it doesn't have a concept of this.x, etc.

    It is the same if I use the more popular way of defining a class by moving the function out:

    function Animation() {
        this.x = ...
        etc...
    }
    
    Animation.prototype.doAnimation = function() {
        //  do something
    }
    Code (markup):
    and it is the same, the second time when doAnimation() is invoked by the setTimeout, it doesn't have a concept of this.x.

    Does someone know what it is? hm... I don't need to use prototype.js and use something like doAnimation.bind(this) ? Can it be a self-contained script that doesn't rely on other framework? Thanks.
     
    winterheat, Oct 4, 2008 IP
  2. dimitar christoff

    dimitar christoff Active Member

    Messages:
    882
    Likes Received:
    62
    Best Answers:
    0
    Trophy Points:
    90
    #2
    
    var foo = {
        init: function(params) {
            options = params;
            setTimeout(this.bar, 1000, this); // you need to bind this to the function as it will run outside the 
        },
        bar: function() {
            alert("bar " + this.options.foobar);
        }
    }
    
    foo.init({foobar: "soap"}); // alert bar soap
    
    PHP:
    this will work in FF2/3 but it will fail in IE6 as it's implementation does not allow you to bind... Works in IE7 anyway,
    no IE6 here to test.

    frameworks can fix this, even if you are trying to be standalone. in mootools you'd do...

    (function() {
    this.method();
    }).delay(1000, this);

    it also has something.bind(this); as a convenient syntax.

    as for your problem in IE6, you can do var _this = this; (as a global) and then _this.doAnim...

    you REALLY could bebefit from mootools and moo.fx if you are serious in your animation ....
    it has all sorts of morths, transitions, css based and other, chaining of events and so forth. oh,
    its also crossbrowser compliant and very pleasant to write with - really extends JS to a new level.
    if you want standalone... thats gonna be hard :)

    EDIT: my bad, just saw you use prototype - surely it has the bind here fixed? :D

    i noticed in vanilla js, on the first iteration of the function in setTimeout, with the bind set to this, this is the object.
    after the first recursive call to self however - this becomes "window". that's crap...
     
    dimitar christoff, Oct 4, 2008 IP
    LogicFlux likes this.
  3. winterheat

    winterheat Peon

    Messages:
    125
    Likes Received:
    0
    Best Answers:
    0
    Trophy Points:
    0
    #3
    thanks for your post. that's right... i tried using prototype.js with this.doAnimation.bind(this) and it works! The only thing is instead of using the whole prototype.js, i would like to just use this bind() function instead... to save file size.

    So it looks like a common pattern that, if we want any callback or the function in setTimeout() to actually call the function ON this object, we need to use foo.bind(this) instead of foo.


    by the way, you know you can get a Virtual PC for free to test in IE 6. It is totally free from Microsoft.

    details at:

    http://www.sitepoint.com/forums/showthread.php?p=3924905
     
    winterheat, Oct 4, 2008 IP
  4. LogicFlux

    LogicFlux Peon

    Messages:
    2,925
    Likes Received:
    102
    Best Answers:
    0
    Trophy Points:
    0
    #4

    
    function createBoundedWrapper(object, method) {
      return function() {
        return method.apply(object, arguments);
      };
    }
    
    Code (markup):
    From a good article on binding: http://alistapart.com/articles/getoutbindingsituations

    Could maybe do something like :

    
    setTimeout(createBoundedWrapper(this, this.mymethod), 1000);
    
    Code (markup):
     
    LogicFlux, Oct 5, 2008 IP
    dimitar christoff likes this.
  5. winterheat

    winterheat Peon

    Messages:
    125
    Likes Received:
    0
    Best Answers:
    0
    Trophy Points:
    0
    #5
    wow, that really works. I wonder you need to do a return method.apply(object, arguments); but cannot do something like object.method() ?

    the other method i tried was to have

    var me = this;

    either at the very beginning of the contructor, and have those functions defined in the contructor to use

    setTimeout(function() { me.doAnimation() }, 33);

    or, if defining the function outside the contructor as

    Animation.prototype.doAnimation() {
      var me = this;
      // ... some code
    
      setTimeout(function() { me.doAnimation() }, 33);  
    }
    Code (markup):
    and they will work, but it is extra effort to add those var me = this; definitions.

    by the way, so we need to only use this technique when it is any call back, such as by setTimeout, or a callback that is invoked when the Ajax is finished (by XHR or by Prototype.js's Ajax class)?
     
    winterheat, Oct 5, 2008 IP
  6. LogicFlux

    LogicFlux Peon

    Messages:
    2,925
    Likes Received:
    102
    Best Answers:
    0
    Trophy Points:
    0
    #6
    Because apply() binds the 'this' variable to whatever you want. I'm not sure why the JS implementations or spec chooses to throw away bindings. Maybe just because once you use a reference then it becomes hard or impossible to decide what object it should actually be bound to, unless you tell it explicitly with apply() or call().

    From the article:


    This is the way I usually do it. I just assign 'this' to a local variable at the top of the enclosing method or whatever. It's not pretty and not clever but it works fine.


    Anytime you want to control what the 'this' variable will be in the callback. Sometimes it doesn't matter, sometimes you don't eve reference 'this' in the callback. YUI has a "scope" parameter (http://developer.yahoo.com/yui/connection/#scope) in the callback object that specifies what object to execute in the scope in, or in other words, what 'this' will be. Mootools and the other popular libraries/frameworks probably have a similar mechanism.
    You should take dimitar's advice and use a framework whenever possible.
     
    LogicFlux, Oct 5, 2008 IP