Archive
Preserving Scope in JavaScript
If you are a JavaScript developer who frequently uses OOP in JavaScript you have most likely run into problems when using timeouts or events. For some reason your function no longer worked. In this article I'll discuss why this happened and how to work around the problem.
Before you read any further, please make sure you are familiar with JavaScript and OOP terminology.
May 11: This article needs to be revised. In a discussion at DHTMLCentral I learned some new things. Please read this discussion together with this article. I currently do not have the time to revise it.
Contents
First things first: Scope
This article is all about scope. If you already know what it is, you can safely skip this chapter.
Scope is the context in which code is being executed. Take a look at this example:
function MyClass(){
this.message = "hello world";
};
var instance = new MyClass();
Using the new operator I've created an instance of the class MyClass and stored this instance in a variable named instance. The new operator first creates a new object, then it calls MyClass which populates this object with fields of it's own. The MyClass function is called a constructor.
Look at the MyClass function, how does it know the object on which it wants to create the message property?
In JavaScript - as in many other programming languages - this problem is solved by providing the constructor function a reference to the object it should populate. This reference is stored in the this keyword and is available in all methods of the class, not only in the constructor:
function MyClass(){
this.message = "hello world";
this.talk = function talk(){
alert(this.message);
};
};
var instance = new MyClass();
instance.talk();
As you can see I've created the method talk, which alerts the message. talk also has a reference to the object, this allows it to access the message property. You can try this.
We have now covered the basics of scope. In JavaScript there are several scopes, but they are irrelevant to this article.
Losing scope
At this point we have created an instance of a class which "talks" to us. What if we want the instance to talk at us after one second?
function MyClass(){
this.message = "hello world";
this.talk = function talk(){
alert(this.message);
};
};
var instance = new MyClass();
setTimeout(instance.talk, 1000);
That didn't work as expected, did it? It returned "undefined". Why's that? The setTimeout function defers the code for one second and then executes it in the window scope. See, we've lost scope.
There's another way you can loose scope: events. I'm sure you have used something similar to the following example on multiple occasions:
var oFoo = document.getElementById("foo");
oFoo.onmouseover = function(){
this.style.backgroundColor = "red";
}
When you mouseover an element with ID foo it's background color becomes red. What would happen if we point the onmouseover event to instance.talk? Again we would get an error, since the this keyword refers to oFoo. We've lost scope.
If you use the Internet Explorer method of attaching events you'll loose scope too. Only the W3C DOM method, addEventListener, preserves scope.
Preserving scope
Now we know how we can loose scope, we want to get it back. There are two problems, though. The function cannot be sure if it has lost scope, nor does it know the correct scope. Finding out if the function has lost scope is easy: compare the current scope to the correct one. This leaves us with one problem: how to find the correct scope?
Solution #1: From the inside out
A function which is executed with the wrong scope knows but one thing about itself, namely: itself. The arguments object has a reference to the function, arguments.callee. We can set a property on the function which refers to the scope, and then use arguments.callee to find this reference. Once we know the correct scope, we can verify if the function has lost scope or not. We do this by using the following code:
if(this != arguments.callee._oScope){
return arguments.callee.apply(arguments.callee._oScope, arguments);
};
this of course refers to the current scope. arguments.callee._oScope refers to the correct scope. If these two objects are not the same, we apply the function to the desired scope and return the result. The return operator also stops the execution of the function with the wrong scope.
Instead of function.apply you can also use function.call. For more information about these methods I direct you to the Netscape's JavaScript 1.5 reference.
The updated MyClass code now looks like this:
function MyClass(){
this.message = "hello world";
this.talk = function talk(){
if(this != arguments.callee._oScope){
return arguments.callee.apply(arguments.callee._oScope, arguments);
};
alert(this.message);
};
// setting reference
this.talk._oScope = this;
};
var instance = new MyClass();
setTimeout(instance.talk, 1000);
Cool, it worked. There are a few caveats with this solution, though. It won't work in older browsers as IE 5.0 because function.apply is not supported. You can work around this by using this piece of code written by Aaron Boodman:
// Implement function.apply for browsers which don't support it natively
// Courtesy of Aaron Boodman - http://youngpup.net
if (!Function.prototype.apply) {
Function.prototype.apply = function(oScope, args) {
var sarg = [];
var rtrn, call;
if (!oScope) oScope = window;
if (!args) args = [];
for (var i = 0; i < args.length; i++) {
sarg[i] = "args["+i+"]";
}
call = "oScope.__applyTemp__(" + sarg.join(",") + ");";
oScope.__applyTemp__ = this;
rtrn = eval(call);
oScope.__applyTemp__ = null;
return rtrn;
}
}
Furthermore, arguments is deprecated in JavaScript 1.5. I don't think this will be a problem, however, as even the latest browsers still support it.
Solution #2: Events only
Another way of finding the right scope is by setting it as a property on the element on which an event invoked the function. I'm not going into details here, once the scope is found the logic used in above code can be adapted to fix the scope.
Of course this solution will not work if you are using a timeout.
Solution #3: Closures
In JavaScript you can share variables between functions. Such a shared variable is called a closure. It is said, though, that closures require quite a lot more memory than ordinary properties.
When we use closures the code looks like this:
function MyClass(){
var self = this; /* reference to the right scope */
this.message = "hello world";
this.talk = function talk(){ /* remember: because I named the function I can access it directly by it's name */
if(this != self){
return talk.apply(self, arguments);
};
alert(this.message);
};
};
var instance = new MyClass();
setTimeout(instance.talk, 1000);
Solution #4 - Know-it-all
If you are using a static class preserving scope gets a lot easier. Take a look at the following example:
var MyStaticClass = {
message : "hello world",
talk : function(){
if(this != MyStaticClass){
return MyStaticClass.talk(arguments);
};
alert(this.message);
}
};
setTimeout(MyStaticClass.talk, 1000);
Conclusion
When you are using OOP in JavaScript you can easily loose scope. There are four solutions to this problem. The fourth solution is desirable if you are using a static class, if not, the first solution is the easiest to implement.
Comments
Leave a Comment
Due to some recent spam, and because this site is no longer active I've decided to disable comments side-wide. You can always mail me if you have questions. Thanks!
trs wrote:
Not a very eloquent solution, at some point there is still global code running so the scope context doesn’t encapsulate the execution. The bottom line is that with functions like setTimeout, the first argument will always run in global scope and there is nothing you can do about it. Probably best use a closure and be careful with references to binded COM objects i.e. DOM nodes (especially in Microsoft Internet Explorer, the MSFT GC has trouble cleaning up after non-JScript owned objects).
May 8, 2004 @ 7:03 pm. Type: Comment. Permalink.
David Schontzler wrote:
I know I’m replying to this real late, but I thought I should point it out…
The most eloquent solution would be to just replace all references to this with self. That way you don’t have to do any scope checking or rewrite your code structure.
September 13, 2004 @ 2:18 am. Type: Comment. Permalink.
Mark Wubben wrote:
David, quite true. This was also pointed out in the linked DHTMLCentral posts (which was lost last Friday).
As you can see in the sIFR code this is more or less the way I’m (currently) hacking.
September 13, 2004 @ 3:05 pm. Type: Comment. Permalink.
Gustavo Armagno wrote:
If I want to pass the value of a variable to an event, there’s another ‘dirty’ (what it’s not dirty in js?) way to solve the problem:
Not working:
function createAnchor() {
var str = “hello!!!”;
var anchor = document.createElement("a");
anchor.href = “”;
anchor.onclick = function() {
alert(str);
}
… [appendChild to some document element]
}
Variable str is out of the scope when the event is triggered.
Solution:
function createAnchor() {
var str = “hello!!!”;
var anchor = document.createElement("a");
anchor.href = “”;
var evalStr = “anchor.onclick = function() { alert(’” + str + ”‘); }”;
eval(evalStr);
… [appendChild to some document element]
}
January 20, 2005 @ 2:55 pm. Type: Comment. Permalink.
Mark Wubben wrote:
Gustavo, actually your first example will work because the
onclickevent is a closure in whichstrdoes exist.January 20, 2005 @ 2:59 pm. Type: Comment. Permalink.
Brent Hendricks wrote:
Mark,
I have a similar situation to the one Gustavo posted, but in my case I’m setting several
onclickhandlers in a loop withstras the iteration variable. So even though the variable exists in the handler, it has the wrong value (the final iteration value). Is there a way to pass the value of a variable without using evalStr?January 22, 2005 @ 7:36 pm. Type: Comment. Permalink.
Mark Wubben wrote:
Brent, interesting question. Here’s an idea (haven’t tested it):
while(str != null){node.onclick = (function(str){
return function(){ alert(str); }
})(str);
};
What this does is calling an anonymous function in block scope with the variable as it’s argument. This function returns a closure in which the variable is available. A new closure is returned every time the anonymous function is invoked, thus the problem of referencing the iteration variable is solved.
January 22, 2005 @ 7:50 pm. Type: Comment. Permalink.