SlightlyLoony
Tera Contributor
Options
- Subscribe to RSS Feed
- Mark as New
- Mark as Read
- Bookmark
- Subscribe
- Printer Friendly Page
- Report Inappropriate Content
11-02-2011
06:26 AM
A few weeks ago I helped someone troubleshoot an interesting JavaScript code problem. The actual case was fairly complicated, and the error wasn't at all obvious to my (very frustrated) colleague, but here's a simple program that demonstrates the equivalent problem:
function Person(first, last) {
this.first = first;
this.last = last;
this.count = 0;
}
Person.prototype.name = function() {
this.count++;
return this.first + ' ' + this.last;
}
var count = 10;
while (count > 0) {
gs.log('Got here');
var p = Person('Slightly', 'Loony');
count--;
}
You'd expect this to print "Got here" 10 times, but it didn't — it only prints once. Do you see why?
The problem is in this line:
var p = Person('Slightly', 'Loony');
The programmer forgot to put the new before the Person('Slightly', 'Loony'). Add that, and everything works fine. But why?
To understand the reason, you need to know (and remember!) a few things:
- Constructors are functions. Unlike many other programming languages, JavaScript uses ordinary functions as constructors. It's perfectly allowable to invoke a constructor as if it were a function — for the simple reason that it really is a function. That what our error of leaving out the "new" did — we just invoked it as an ordinary function.
- Functions have "this" references. The "this" reference is what lets functions be methods of object instances. When an instance method (which is really just a function) is invoked, the "this" reference is automatically initialized to the instance we're calling the method on.
- The "new" keyword does stuff. In particular, it creates a new, uninitialized instance of the object we're creating, and it passes a reference to that object as the "this" pointer when invoking the constructor function. This is how the constructor knows what instance to initialize.
- Functions invoked outside the context of an instance are passed the global object as "this". Normally a function designed to be invoked outside the context of any instance (in other words, functions that aren't methods) wouldn't use the "this" pointer at all. But if they did, they'd find that it was set to the global object.
Now we can explain what happened in our program. When the erroneous line was run, the Person function was being called outside the context of any instance. Therefore its "this" reference was set to the global object. When the constructor function executed this line:
this.count = 0;
It set the global variable count to 0. So in this loop:
var count = 10;
while (count > 0) {
gs.log('Got here');
var p = Person('Slightly', 'Loony');
count--;
}
Even though count was set to 10 to start with, when we executed the Person function, it got set to 0. The following count-- set it to -1, and then the loop terminated.
All that mess because the "new" was accidently left out.
Can you protect your JavaScript classes against such an accident. Yes, you can. One thing you can do is this:
function Person(first, last) {
if (this === JSUtil.getGlobal())
throw new Error('You forgot the "new", you hamburger!');
this.first = first;
this.last = last;
this.count = 0;
}
We just added a test to see if the "this" reference was identical to the global object, and if it was, we throw an error. Note that the JSUtil.getGlobal() is specific to the JavaScript environment on a ServiceNow instance; on the client side you would use the window object.
So now if someone forgets the "new", they'll get a big old nasty error, but at least you won't get these crazy, hard to troubleshoot side effects. But can we do even better than this? Of course:
function Person(first, last) {
if (this === JSUtil.getGlobal())
return new Person(first, last);
this.first = first;
this.last = last;
this.count = 0;
}
Now if someone leaves out the "new", we supply it for them! This works just fine, and basically has the effect of making the "new" keyword completely optional. Some built-in JavaScript objects work exactly like this. For example, both lines below do the same thing:
var x = new Boolean('true');
var y = Boolean('true');
This is how JavaScript allows certain constructor functions to be used both for new instance construction and as if they were conversion functions. You can use the same trick in your own classes. Note that there is no requirement for the return value to be of any particular type — we could have returned a number or a string just as well as a Person.
Ah, the things we never new!
2 Comments
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.