views:

427

answers:

4

I'm sure there's a simple answer to this, but it's Friday afternoon and I'm tired. :(

Not sure how to explain it, so I'll just go ahead and post example code...


Here is a simple object:

var Bob =

{ Stuff : ''

, init : function()
 {
  this.Stuff = arguments[0]
 }

, doSomething : function()
 {
  console.log( this.Stuff );
 }

}

And here it is being used:

$j = jQuery.noConflict();
$j(document).ready( init );


function init()
{
 Bob.init('hello');

 Bob.doSomething();

 $j('#MyButton').click( Bob.doSomething );
}

Everything works, except for the last line. When jQuery calls the doSomething method it is overriding 'this' and stopping it from working.

Trying to use just Stuff doesn't work either.

So how do I refer to an object's own properties in a way that allows jQuery to call it, and also allows the object to work with the calling jQuery object?

i.e. I would like to be able to do things like this:

doSomething : function()
    {
     console.log( <CurrentObject>.Stuff + $j(<CallerElement>).attr('id') );
    }

(Where <CurrentObject> and <CallerElement> are replaced with appropriate names.)

+7  A: 

Change

$('#MyButton').click( Bob.doSomething );

to

$('#MyButton').click( function() { Bob.doSomething() } );

You could also add to your Bob object a private field var that = this and use that everywhere in members instead of this if you really want to avoid the anonymous function.

e.g.

var Bob = new function() {
    // Private Fields
    var that = this;

    // Public Members   
    this.Stuff = '';

    this.init = function() {
                that.Stuff = arguments[0];
        }

    this.doSomething = function() {
                console.log( that.Stuff );
        }
}
nickyt
To clarify why this works: jquery binds `this` to be the element that fired the event when doing callbacks. Therefore it's trying to execute `someElement.doSomething()`
TM
Ugly. Also, how do I then access the jQuery object for the calling object?
Peter Boughton
I have clarified the question - by default this suggestion will prevent `doSomething` from knowing that `#MyButton` called it, which is not the intent, and doing `$j('#MyButton').click( function(){Bob.doSomething(arguments)} );` results in nested arguments which is icky, so I'm hoping there's a nicer solution.
Peter Boughton
+8  A: 

This is not jQuery's fault, it is integral to the way JavaScript handles objects.

Unlike in most other object-oriented languages, ‘this’ is not bound on a per-method level in JavaScript; instead, it's determined purely by how the function is called:

Bob= {
    toString: function() { return 'Bob!'; },

    foo: function() { alert(this); }
};
Brian= {
    toString: function() { return 'Brian!'; },
};

Bob.foo(); // Bob!
Bob['foo'](); // Bob!

Brian.foo= Bob.foo;
Brian.foo(); // Brian! NOT Bob

var fn= Bob.foo;
fn(); // window NOT Bob

What you are doing in the case of:

$j('#MyButton').click( Bob.doSomething );

is like the last example with fn: you are pulling the function doSomething off Bob and passing it to jQuery's event handler setter as a pure function: it no longer has any connection to Bob or any other object, so JavaScript passes in the global window object instead. (This is one of JavaScript's worst design features, as you might not immediately notice that window isn't Bob, and start accessing properties on it, causing weird and confusing interactions and errors.)

To remember Bob, you generally make a function as in nickyt's answer, to keep a copy of ‘Bob’ in a closure so it can be remembered at callback time and used to call the real method. However there is now a standardised way of doing that in ECMAScript Fifth Edition:

$j('#MyButton').click( Bob.doSomething.bind(Bob) );

(You can also put extra arguments in the bind call to call doSomething back with them, which is handy.)

For browsers that don't yet support Fifth Edition's bind() method natively (which, at this point, is most of them), you can hack in your own implementation of bind (the Prototype library also does this), something like:

if (!Object.bind) {
    Function.prototype.bind= function(owner) {
        var that= this;
        var args= Array.prototype.slice.call(arguments, 1);
        return function() {
            return that.apply(owner,
                args.length===0? arguments : arguments.length===0? args :
                args.concat(Array.prototype.slice.call(arguments, 0))
            );
        };
    };
}
bobince
Thanks for the thorough explanation... though sounds like I'm not going to manage exactly what I want. :(
Peter Boughton
This is all really useful information, but I find its usually easier to just avoid using `this` in the first place.
Gabe Moothart
+1  A: 

The identity of this is a common problem in javascript. It would also break if you tried to create a shortcut to doSomething:

var do = Bob.doSomething;
do(); // this is no longer pointing to Bob!

It's good practice to not rely on the identity of this. You can do that in a variety of ways, but the easiest is to explicitly reference Bob instead of this inside of doSomething. Another is to use a constructor function (but then you lose the cool object-literal syntax):

var createBob = function() {
    var that = {};

    that.Stuff = '';
    that.init = function() {
        that.Stuff = arguments[0];
    };

   that.doSomething = function() {
       console.log( that.Stuff );
   };

   return that;   
}

var bob = createBob();
Gabe Moothart
This seems like the best compromise (although my real objects are much longer names, unfortunately). Given that I'm effectively 'disconnecting' doSomething from Bob, will referring to Bob inside of doSomething have any other potential side-effects?
Peter Boughton
@Peter It shouldn't in normal usage. You may be able to come up with a convoluted scenario where you assign Bob.doSomething to a variable, and then change the value of Bob, breaking your doSomething shortcut. Those don't usually come up in practice, though. I've also added a second suggestion which is more encapsulated.
Gabe Moothart
A: 

You could always try doing something like this:

$j('#MyButton').click(function(event) {
  Bob.doSomething(event, this);
});

Now doSomething will still have Bob for its this, and using the function arguments, it will have the event object, and the element it was triggered on.

gnarf