2006/07/17

How to preserve scope in Javascript function calls

Developing all of your application logic in Javascript is a bit of a challenge if you grew up in a world of full object orientation and languages like C++, Java and C#. I still have a very strong OO inclination, so I use it in my code as much as possible. I feel this makes things easier to understand and better organized. Leaving dozens or hundreds of loose functions and variables in your code makes things so much harder for everyone. In my opinion it's better to know precisely what object knows X and what object is responsible for doing Y. I'm sure I'm not alone on this. Please stop using globals.

In my own personal experience, and after reading thousands of lines of Mozilla code, I've come to the conclusion that plain classes and objects are pretty rare in XUL code. It's much more common to use the so-called static classes, which are kind of like singletons. The syntax to create such an object goes like this:

var MyObject = {
attribute1 : 5,
attribute2: null,
function1 : function() {alert(this.attribute1); },
function2 : function(param1, param2) { /* ... */ }
}

You get the idea. The MyObject object doesn't really have a class. It's a class and an instance declaration, all in one. You can call its methods or get or set its attributes (obviously not recommended, that's what methods are for) in the usual way:

someResult = MyObject.function1();
MyObject.attribute1 = 7;

You can use the "this" identifier inside the object just like you would on any object, and all attributes preserve their state as long as the object exists.

But there's a problem with this, and maybe you already ran into it. Under certain circumstances you reach for your attributes and end up with uninitialized variables that are not what you expected. This probably happened because you used the method of an object as a callback function for some asynchronous event. Classic examples of this are the "window.setTimeout" function, event handlers and observers. You're probably passing a pointer to your method like this:

window.setTimeout(MyObject.function1, 1000);

This is OK. The problem happens on the other end. Say you execute the following code:

MyObject.attribute1 = 10;
window.setTimeout(MyObject.function1, 1000);

You'll notice the alert box is showing the value "5" as opposed to "10". All variables will be uninitialized, and you'll find yourself as if you had a completely different object. And you do. Since these callbacks occur in a different scope, they don't see the MyObject object we created. They call "function1" as if it were any loose function, so it runs completely out of context. I don't entirely understand what goes on here, but we don't need to. What we need is a solution.

Some valuable feedback made me change the preferred solution in this post. There are some important aspects to consider here, such as elegance and readability of code. My first solution was elegant in that it doesn't use anonymous functions - which I avoid like the plague - but it fails in being very nice to look at. It looks too much like a hack. The solution proposed in the comments below is much more easier to look at, albeit relying on the dreaded anonymous functions. It is also more general. As you'll see in the explanation for the old solution, this only works for static objects, and uglier hacks are required for regular objects or XBL inner objects.

The old solution - do not use!

You rewrite "function1" like this:

function1 : function() {
if(this != MyObject) {
return MyObject.function1();
}
alert(this.attribute1);
},

This odd hack is enough to fix the problem. The "if" statement checks whether the method is being called in the right scope. If it isn't, the right call is performed, and the first call returns without executing the rest of the code. It's the second one, the method inside our object, that does all the work. Now we'll see the "10" we wanted.

You may feel tempted to just add this code to all of your functions and don't worry about this problem ever again. Don't. This is not a pretty hack, and adding so much code is very unnecessary. It's also better that you realize when it's required and when it isn't.The linked reference, Preserving Scope in JavaScript, shows a wider array of solutions which you may find handy for other situations of lost scope.

The new solution

Rewrite the asynchronous scheduling like so:

window.setTimeout(function(){MyObject.function1();}, 1000);
Easy, right? Now the asynchronous call will be performed on the anonymous function, which will in turn call the function in our object, this time in the way we expect. The same will work with instances of regular classes, or functions inside XBL bindings.
You may feel tempted to add more and more code inside the anonymous function, but I strongly recommend against this practice. Anonymous functions make it very hard to debug and read Javascript code. Please use them with moderation, preferably only as proxies for a single function or method call.

Labels: , , , ,


Comments:
Ew.

First, here's an explanation of the problem (which has nothing to do with the "scope"): http://developer.mozilla.org/en/docs/Core_JavaScript_1.5_Reference:Operators:Special_Operators:this_Operator#Method_binding

Second, the presented solution ("if(this != MyObject) {") is just ugly. I prefer:

setTimeout(function() {myObject.func()}, 100);

Nickolay
 
I guess that depends on what you mean by scope. But you're right, technically is not the same. I was referring to ownership, I guess.
Your solution works as well, but I think that one's pretty ugly too. I'm not a fan of anonymous functions. They tend to grow and make things unreadable and hard to debug.
Thanks for the link and the alternative, though :).
 
Yeah, it's ugly too, but not as ugly as the |this| check :)
You can give the function a name if you like.

There are two reasons I think this is better than your solution:
1) It also works in cases the code in the callback method doesn't know the owner object's name. For example:
function WeirdGuy(name) {
this.name = name;
}
WeirdGuy.prototype.sayHi = function() {
alert("Hi from " + this.name);
};
var alan = new WeirdGuy("Al");

document.addEventListener("load", function(ev) {alan.sayHi(ev);}, false);

2) You know what the value of |this| is going to be. When you call it as usually (obj.func()), it's set correctly. When it is invoked as a callback, |this| is messed up. You only need to apply the fixup in the latter case.

BTW, making people register with blogger *and* enter captcha is cruel :)
 
Fair enough. You beat me in less-ugliness :P. I'll fix the article as soon as I can.
The insane validations are to prevent spamming. Sorry for the inconvenience, everybody. For what it's worth, I have to write it too when I comment.
 
Post a Comment



<< Home

This page is powered by Blogger. Isn't yours?