Thursday, June 09, 2005

Javascript surprises

Javascript is not a wholly unpleasant language to work in, but it has its share of surprises.

Lexical scope: Functions are first-class values, and variables declared within the var keyword are sort of lexically scoped with respect to the nesting of functions. However, despite the C-like syntax, there is no block-style scoping. This means that you can write strange-looking functions like
function f(b) {
if (b) {
var x = 3;
}
alert(x);
}
I think the meaning of this function is that the lexical scope of x is lifted to the entire function body, but if the if-branch isn't taken, x has the default undefined value. This means that in the following example:
var x = 2;

function g(b) {
if (b) {
var x = 3;
}
alert(x);
}
the variable x is always shadowed in the body of g, so that it displays either 3 or undefined, and never 2.

Furthermore, you can assign to variables that have not been declared with var, which is subtly necessary because of the dynamic scope of this (see below). Basically, if you assign to a variable that hasn't been declared lexically with var, the language first searches the dynamically scoped this object for a member of that name, and then up the prototype chain. Somewhat unfortunately, if it doesn't find any member of the given name, it silently generates a new global variable of that name and performs the assignment.

this is everywhere: There is always a special this variable available, but its binding is dynamically scoped. The upshot is that you can create an ordinary function that refers to this in its body, and then later assign that function as a member of an object, and the this automatically gets wired to the new container.

Bizarrely, there is a special "global" object, so that if such a function is not a member of any object, this refers to that special object; even more bizarre is that particular Javascript engines seem to be free to make that global object whatever they want. In DHTML engines it's some kind of window object or something, whereas in Rhino it might be something else (this is hearsay--I haven't tried it). Furthermore, all global variables are actually member variables of this global object.

It gets even weirder: this means that global function declarations of the form
function foo() { ... }
are really just syntactic sugar for
foo = function() { ... };
which is a declaration of a global variable, i.e., a member variable of the global object, whose value is a function. It's truly strange, but it has a sort of internal consistency, and you kind of have to admire their chutzpah.

Object system: Javascript is the only prototype-based OO language I've ever worked with, so this was all new to me. To start with, objects are essentially just associative arrays, i.e., tables mapping names to values, much like Python or Lua. But every object is also linked to a "prototype" object, and name resolution searches the chain of prototype references. This is quite different from class-based inheritance, but it does provide a kind of dynamic dispatch, so it serves similar purposes. (It can also be used to simulate class-based inheritance.) The prototype of an object is exposed via its prototype member variable, which is mutable like everything else in Javascript.

Rather than being defined in classes, constructors in Javascript are simply functions that, when called with the new operator, have their new object implicitly available via this. By convention, people usually capitalize the name of constructor functions, which makes it look like Java-style, class-based object creation. But that "class name" is in fact just an ordinary Javascript function.

for ... in: In addition to the ordinary integer-indexed for loops, Javascript has a for ... in syntax. But it's not at all what you'd expect! This special form is only used to range over the keys (i.e., member names) in an object. It has nothing to do with iterating over collections. Specifically, if you use this to try to use this form on an array, you end up iterating over all the names of the member variables of the array object (which includes the elements of the array, because they are also member variables, whose names are integers! wow). That one took some head-scratching the first time I encountered it.

Extension by mutation: It seems an industry standard form of "extension" is to mutate the prototype of an object. Lord help you if you accidentally overwrite an existing method. What, me, namespaces?

IE has "alien" types: All right, this isn't Javascript's fault. But it's annoying. In IE, the elements of the core DOM are not actually derived from the standard Object prototype, and their prototypes are not even visible. So there's no way to extend the behavior of the core DOM prototypes.

3 comments:

Noel Welsh said...

If only I'd read this when I started Javascript programming, instead of the endless waffle designed for morons, things would have been a lot clearer! I think there would be a (small) market for books on popular programming languages written for theory geeks. Kinda the opposite to the 'Dummies' series.

Dave, if you're doing anything interesting in Javascript drop me a line. The multi-stage language idea is something I'm sporadically working on.

Sjoerd Visscher said...

"function foo() { ... }"
is not just syntactic sugar for
"foo = function() { ... };"

"foo(); function foo() { ... }"
works, but
"foo(); var foo = function() { ... };"
does not.

Dave Herman said...

Noel: It's a frightening wilderness out there, isn't it? Stay tuned; I'll post the most useful links I've found so far.

Sjoerd: Right you are. I guess there's some kind of prepass to discover all top-level function declarations.