Binding Javascript Method References To Their Parent Classes
NOTE: Dan G. Switzer, II caught a huge error in my original post. I was using call() rather than apply(). This has been updated in the following demonstration.
For those of you who have used Classes in Javascript, you probably have noticed that function references used within the class methods are not actually bound to the class instance in which they were defined. By that, I mean that if you do something like create an AJAX callback method, the callback method is not run in the context of the parent object, but rather in the context of the window. To get around this in the past, I have created local variables that point to the THIS scope and then referenced those in the callback methods:
var objSelf = this;
someMethod(
function(){
objSelf.DoSomething();
}
);
In this example, because the someMethod() callback function is defined inline within it's parent context and then passed out, it creates a closure. This allows the objSelf variable referenced within the callback method to actually refer to the original parent context. In case you are totally confused, what we are doing here is substituting objSelf for the "this" keyword since the "this" keyword will actually refer to the window object when the callback method gets executed (not the original parent context).
Yesterday, I came across an interesting "A List Apart" article titled, "Getting Out of Binding Situations in JavaScript". In it, the author Christophe Porteneuve demonstrates how to use the Javascript apply() method to bind a function reference to its parent context such that work-arounds like the one above don't have to be used. Below, I have put together a little demonstration of how this works:
<script type="text/javascript">
// Define the base class for all object classes.
function BaseObject(){
// Class properties.
}
// Method returns a class-bound vesion of the passed-in
// function; this will execute in the context of the
// originating object (this).
BaseObject.prototype.Bind = function( fnMethod ){
var objSelf = this;
// Return a method that will call the given method
// in the context of THIS object.
return(
function(){
return( fnMethod.apply( objSelf, arguments ) );
}
);
}
// ------------------------------------------------ //
// ------------------------------------------------ //
// Extend the base object.
Person.prototype = new BaseObject();
// Define person class.
function Person( strName ){
this.Name = strName;
}
// This method will wait a given amount of time and
// then ping the given person.
Person.prototype.Ping = function( objPerson ){
setTimeout(
this.Bind(
function(){
// Because this method is being bound,
// the THIS scope referenced below
// refers to the originating object, not
// the WINDOW object.
alert( "Hey " + objPerson.Name + ", it's " + this.Name );
}
),
2000
);
}
// ------------------------------------------------ //
// ------------------------------------------------ //
// Create two people.
var objBen = new Person( "Ben" );
var objSarah = new Person( "Sarah" );
// Call one of them shortly.
objBen.Ping( objSarah );
</script>
It's a small demo, but it's complex, so let's walk through it. First, I am creating a BaseObject class that will act as a base class for all other Javascript classes to extend (this extension is explicit, not implicit). I am doing this because I am trying to get more in the habit of thinking about objects. The base class has a single method: Bind():
BaseObject.prototype.Bind = function( fnMethod ){
var objSelf = this;
// Return a method that will call the given method
// in the context of THIS object.
return(
function(){
return( fnMethod.apply( objSelf, arguments ) );
}
);
}
This method takes a method reference and the returns an inline method definition that, in turn, executes the passed-in method in the context of the parent object. Now, it's true that we are still using the "objSelf" hack here, but because we are using it to bind the method reference to the parent object, we won't have to worry about it anywhere else (including the body of the original method reference).
Once we have this method in place, we can now use it to bind our callback function references to the parent object as in our Person.Ping() method:
Person.prototype.Ping = function( objPerson ){
setTimeout(
this.Bind(
function(){
// Because this method is being bound,
// the THIS scope referenced below
// refers to the originating object, not
// the WINDOW object.
alert( "Hey " + objPerson.Name + ", it's " + this.Name );
}
),
2000
);
}
Notice that in our setTimeout() method call, we are not passing our callback method directly in as the first argument; rather, we are passing the method reference to our bind method (this.Bind()), which will create a bound version of it and then pass that method in as the first setTimeout() argument. Because we are essentially decorating our method reference with this binding, we can now refer to the "this" keyword within our callback method and know that it will point to the original Person object instance.
Now, when we run the above code, we get the following alert:
Hey Sarah, it's Ben
Had we not bound our method, we would have gotten this:
Hey Sarah, it's undefined
This is because the callback method has no implicit binding to the parent Person instance; and, when it gets run, it would think that this.Name is referring to window.Name, which is undefined.
This is a really awesome technique and will allow us to use a more object-oriented mindset. However, I just wanted to caution about overusing it! A lot of Javascript libraries such as jQuery purposely set the THIS context of callback methods, such as when handling events or iterating over collections. In that case, I would say you don't want to override the explicit "this" context (or do so with a full understanding).
Want to use code from this post? Check out the license.
Reader Comments
Don't forget about the apply() method--which is even handier (and what I prefer:)
function Person(name){
this.name = name;
this.setAge = function (age, dob){
// if we have an onSetAge callback defined
if( !!this.onSetAge ) this.onSetAge.apply(this, arguments);
}
}
Person.prototype.onSetAge = function (){
// we get the same arguments passed to the setAge() method
console.log(arguments);
}
var bo = new Person("Dan");
bo.setAge("Dan", "Feb 28, 1972");
Actually, looking at your example and the use of fnMethod.call( objSelf, arguments ) I think you've confused apply() and call().
The difference is apply() takes an array of arguments as the second argument, where call() uses the first argument as a reference to an object and then each additional argument is passed as it's own argument:
function test(p1, p2, p3){
}
function test2(p1, p2, p3){
test.call(this, p1, p2, p3);
}
As mentioned in the A List Apart article, Prototype has a great function called "bind" which helps with this sort of thing. Although, doesn't it look a bit weird?
<pre>
iterator.each(
function(e) {
this.something();
}.bind(this)
)<pre>
@Dan,
Oops! You are exactly correct. I have reversed the call() and the apply()! I agree - the apply() is way more useful. Great catch, thanks!
@Joe,
Yeah, I am not crazy about that notation; but, really cool if you are already using Prototype.
@Dan,
Great catch - the post has been updated.
@Dan,
Looks like I had it backwards on this post as well:
www.bennadel.com/index.cfm?dax=blog:1514.view
Dang, lots of saves today :)
Very great post thanks for your insight!! :D
jQuery now makes this super easy and powerful with their .proxy() method:
www.bennadel.com/blog/2001-Using-jQuery-s-Proxy-Method-In-Event-Binding-And-Unbinding.htm
It even allows you to unbind proxied event handlers using the original function references.
Thanks Ben,It really helped me.
@Pratik,
Glad you found this helpful. Hopefully, one day, enough of the old browsers die out that we can start using the native Function.prototype.bind() feature in HTML5:
http://kangax.github.io/compat-table/es5/#Function.prototype.bind
Or you can use the polyfill for .bind. There are some differences to the native implementation that you'll have to keep in mind though.
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/bind#Compatibility
Thanks Ben, helped me get out of a tough situation