views:

489

answers:

4

I'm just getting into using prototypal JavaScript and I'm having trouble figuring out how to preserve a this reference to the main object from inside a prototype function when the scope changes. Let me illustrate what I mean (I'm using jQuery here):

MyClass = function() {
  this.element = $('#element');
  this.myValue = 'something';

  // some more code
}

MyClass.prototype.myfunc = function() {
  // at this point, "this" refers to the instance of MyClass

  this.element.click(function() {
    // at this point, "this" refers to the DOM element
    // but what if I want to access the original "this.myValue"?
  });
}

new MyClass();

I know that I can preserve a reference to the main object by doing this at the beginning of myfunc:

var myThis = this;

and then using myThis.myValue to access the main object's property. But what happens when I have a whole bunch of prototype functions on MyClass? Do I have to save the reference to this at the beginning of each one? Seems like there should be a cleaner way. And what about a situation like this:

MyClass = function() {
  this.elements $('.elements');
  this.myValue = 'something';

  this.elements.each(this.doSomething);
}

MyClass.prototype.doSomething = function() {
  // operate on the element
}

new MyClass();

In that case, I can't create a reference to the main object with var myThis = this; because even the original value of this within the context of doSomething is a jQuery object and not a MyClass object.

It's been suggested to me to use a global variable to hold the reference to the original this, but that seems like a really bad idea to me. I don't want to pollute the global namespace and that seems like it would prevent me from instantiating two different MyClass objects without them interfering with each other.

Any suggestions? Is there a clean way to do what I'm after? Or is my entire design pattern flawed?

A: 

Since you're using jQuery, it's worth noting that this is already maintained by jQuery itself:

$("li").each(function(j,o){
  $("span", o).each(function(x,y){
    alert(o + " " + y);
  });
});

In this example, o represents the li, whereas y represents the child span. And with $.click(), you can get the scope from the event object:

$("li").click(function(e){
  $("span", this).each(function(i,o){
    alert(e.target + " " + o);
  });
});

Where e.target represents the li, and o represents the child span.

Jonathan Sampson
+1  A: 

You can set the scope by using the call() and apply() functions

Gabriel McAdams
+6  A: 

For preserving the context, the bind method is really useful, it's now part of the recently released ECMAScript 5th Edition Specification, the implementation of this function is simple (only 8 lines long):

// The .bind method from Prototype.js 
if (!Function.prototype.bind) { // check if native implementation available
  Function.prototype.bind = function(){ 
    var fn = this, args = Array.prototype.slice.call(arguments),
        object = args.shift(); 
    return function(){ 
      return fn.apply(object, 
        args.concat(Array.prototype.slice.call(arguments))); 
    }; 
  };
}

And you could use it, in your example like this:

MyClass.prototype.myfunc = function() {

  this.element.click((function() {
    // ...
  }).bind(this));
};

Another example:

var obj = {
  test: 'obj test',
  fx: function() {
    alert(this.test + '\n' + Array.prototype.slice.call(arguments).join());
  }
};

var test = "Global test";
var fx1 = obj.fx;
var fx2 = obj.fx.bind(obj, 1, 2, 3);

fx1(1,2);
fx2(4, 5);

In this second example we can observe more about the behavior of bind.

It basically generates a new function, that will be the responsible of calling our function, preserving the function context (this value), that is defined as the first argument of bind.

The rest of the arguments are simply passed to our function.

Note in this example that the function fx1, is invoked without any object context (obj.method() ), just as a simple function call, in this type of invokation, the this keyword inside will refer to the Global object, it will alert "global test".

Now, the fx2 is the new function that the bind method generated, it will call our function preserving the context and correctly passing the arguments, it will alert "obj test 1, 2, 3, 4, 5" because we invoked it adding the two additionally arguments, it already had binded the first three.

CMS
I really like this functionality but in a jQuery environment I'd be inclined to name it something else given the existing jQuery.bind method (even though there's no actual naming conflict).
Rob Van Dam
@Rob, it's trivial to make a `jQuery.bind` method that behaves in the same way as `Function.prototype.bind`, check this simple implementation: http://jsbin.com/aqavo/edit although I would consider changing its name, it could cause confusion with the Events/bind method...
CMS
I would strongly recommend sticking with the name `Function.prototype.bind`. It's now a standardised part of the language; it's not going away.
bobince
@bobnice: Totally agree, the native implementation will be available soon in major JavaScript engines... https://bugs.webkit.org/show_bug.cgi?id=26382 https://bugzilla.mozilla.org/show_bug.cgi?id=429507
CMS
Good to know about the browser bugs. FYI, jQuery 1.4 now includes [jQuery.proxy](http://api.jquery.com/jQuery.proxy/) with similar (although not identical) functionally. Use like this `$.proxy(obj.fx, obj)` or `$.proxy(obj, "fx")`
Rob Van Dam
+3  A: 

For your last MyClass example, you could do this:

var myThis=this;
this.elements.each(function() { myThis.doSomething.apply(myThis, arguments); });

In the function that is passed to each, this refers to a jQuery object, as you already know. If inside that function you get the doSomething function from myThis, and then call the apply method on that function with the arguments array (see the apply function and the arguments variable), then this will be set to myThis in doSomething.

icktoofay
That won't work, by the time you get to this.doSomething, `this` has already been replaced by jQuery with one of the elements.
Rob Van Dam
Yeah, it had two problems when I originally posted it. I edited it, and now it should work. (sorry about that...)
icktoofay