1

Is it possible to somehow invoke a non-global function (i.e. a method of a class) from a timer?

The following example shows what I'd like to achieve:

function MyClass() {
  var msg = 'no message';

  // shows an alert after some delay
  this.delayedAlert = function(message) {
    msg = message;

    // global function works:
    //window.setTimeout('globalShow("'+message+'")', 1000);
    // this obviously also works:
    //this.show();

    // this doesn't work: *************
    // I'd like to invoke the show() method of this class after some delay
    window.setTimeout('this.show()', 2000);
  };
  this.show = function() {
    alert(msg);
  };
}

$(document).ready(function() {
  $('p').click(function() {
    var c = new MyClass();
    c.delayedAlert('hello');
  });
});

function globalShow(msg) {
   $('#hello').html(msg);
}

You can find a running sample here: http://jsbin.com/aqako5

M4N
  • 94,805
  • 45
  • 217
  • 260

3 Answers3

4
function MyClass() {
  var msg = 'no message';

  // shows an alert after some delay
  this.delayedAlert = function(message) {
    msg = message;

    window.setTimeout($.proxy(this.show, this), 2000);
  };
  this.show = function() {
    alert(msg);
  };
}

Please use functions in setTimeout. Strings call eval and eval is like goto. I.e. the raptors get you.

What's actually happening is that inside the function in setTimeout the this value is the default this value, i.e. window. You need to pass in a proxied method so that this is what you want this to be.

An alternative pattern to proxying in the correct this value is :

function MyClass() {
  var msg = 'no message';
  var that = this;

  // shows an alert after some delay
  this.delayedAlert = function(message) {
      msg = message;

      window.setTimeout(function() {
          that.show();
      }, 2000);
    };
    this.show = function() {
        alert(msg);
    };
}

This involves "caching" the correct this value in a separate variable. You still can't pass in that.show directly because that function will be invoked without knowing what this is.

Raynos
  • 166,823
  • 56
  • 351
  • 396
  • Great! And (now) obvious. Thanks – M4N Apr 01 '11 at 16:11
  • You say to use functions rather than strings in 'setTimeout()'. But when I do that, it calls the function immediately rather than after the set delay. The inconsistency is that when I use functions which don't have parameters, then I can just put the function name without parentheses  and then it works ok (called after delay). But when I need to pass an argument, I'm forced to add the parentheses . Any suggestions or workaround? – Jeach Jul 09 '12 at 17:02
2

This would help. Also to improve your class: if your class is not instantiated, it may cause problems. What would happen if someone called your class like so:

var myClass = MyClass();

The this inside MyClass would now be window, causing all sorts of problems and adding your properties to the global namespace.

To fix this, you need to make sure that when your class function is being called, that it was done with the new operator. You need to check inside MyClass that if this isn't a new instance of MyClass to force it to be a new instance. With my code below, you can call this method either of the following ways and have them both function the same way:

var myClass = MyClass();
// or
var myClass = new MyClass();

Change your class to this:

function MyClass() {
    // if this isn't an instance, make it so:
    if (!(this instanceof MyClass)) {
        return new MyClass();
    }

    var _this = $(this);    

    var msg = 'no message';
    var show = function() {
        alert(msg);
    };    

    // shows an alert after some delay
    this.delayedAlert = function(message) {
        msg = message;
        window.setTimeout(show, 2000);
    };
}

You can also improve this code by not creating a new function definition every time an instance of MyClass is created. Add your methods to the prototype of MyClass:

(function(window) {

    var MyClass = function() {
        // if this isn't an instance, make it so:
        if (!(this instanceof MyClass)) {
            return new MyClass();
        }
    };

    var msg = 'no message';
    var show = function() {
        alert(msg);
    }; 

    MyClass.prototype.delayedAlert = function(message) {
        msg = message;
        window.setTimeout(show, 2000);
    };

    window.MyClass = MyClass;

})(window);
Eli
  • 17,397
  • 4
  • 36
  • 49
  • Can you explain the 'if this isn't an instance, make it so' part in more detail? thanks – M4N Apr 01 '11 at 16:19
  • Thanks again. BTW: I knew about adding methods to the prototype, but this is not related to my question. – M4N Apr 01 '11 at 16:33
  • 1
    I would recommend againts this pattern as you get into the habit of inconstantly using or not using `new`. This just creates a bad habit. – Raynos Apr 01 '11 at 16:51
  • This is the pattern used by most major javascript libraries, nicolas zakas, and stoyan stefanov, some of the most well known front end engineers in the business. If they are using it and recommend it, I would say its a good pattern to use. – Eli Apr 02 '11 at 01:02
0

You could declare an anonymous function:

window.setTimeout(function () {
    this.show();
}, 2000);

And, depending on the function, you could just pass the function itself to setTimeout:

window.setTimeout(this.show, 2000);

When you pass a string to setTimeout, you're basically telling the browser to wait a certain amount of time, and then eval the string.

When the browse evals "this.show()", it thinks that this refers to the global object, or the window.

The first suggestion uses closures, the second just passes the function itself (without executing it!), which we can do because in JavaScript, functions are first-class objects and can be treated just like any other variable.

Just keep in mind that when you use the second solution, you are divorcing the method from its context. If you try the following...

var msg = 'sadface';

var myObj = {
    msg: 'hello world',
    greet: function () {
        alert(this.msg);
    }
};

window.setTimeout(myObj.greet, 2000);

...then the this in greet will no longer refer to myObj. It will instead point to the window. It will alert sadface, not hello world.

Good luck.

Community
  • 1
  • 1
sdleihssirhc
  • 42,000
  • 6
  • 53
  • 67