Understanding JavaScript, maybe
Posted: Tue Jan 11, 2011 10:28 pm
I’ve been talking recently about prototype chains and constructors and how everyone should have been spelling
All but the most primitive programming languages have some sort of type system, a way of formalizing the fact that while text, numbers and spaceships are all buckets of bytes to the machine, they mean different things at the higher level of abstraction where the programmer works. Types can be associated with labels, such as variables and properties, or with values; in JavaScript, they’re always associated with values. (This is known as dynamic typing, as opposed to static typing.)
JavaScript almost has a very simple, uniform type system, in which every value is either an object,
In JavaScript, an object is a collection of properties. Each property has an identifier (which can be a string or a number), some attributes specifying things like whether it can be modified, and a value. If the value is a function (or, technically, any callable object), it’s called a method. There is one special rule for methods: if you call a function by referring to it as a property of an object, that object is accessible as
One of the complications that would be nice to gloss over, but is going to be quite important, is that properties can be faked. Normally, you set the value of a given property and it stays set until you change it, but there is an alternative: a property can be based on accessors, where a function is called to get the value (and, optionally, a second function is called to set the value). Historically, it was only possible for host objects – i.e., ones defined by Oolite or the JavaScript engine – to create such properties, but there is a SpiderMonkey extension to do it from scripts and ECMAScript 5th Edition adds a new, standardized way. Host objects mostly do it for performance; for instance, instead of setting the
The model as described so far is strictly sufficient, but it does have an important flaw: to create multiple similar objects, it would be necessary to create each object and then set each property for it, even if it’s a default value or an accessor-backed property. Most object-oriented programming systems address this problem using a concept called a class, where each object belongs to a fixed class and each class represents one possible set of properties, and classes can be based on other classes forming an inheritance hierarchy. JavaScript uses a simpler yet more flexible model, in which each object can inherit behaviour from another object, known as its prototype. If you attempt to access a property of an object, but the property does not exist, the prototype is consulted, and if necessary its prototype in turn; this is known as following the prototype chain.
Say
If you set
Transferring this to Oolite objects, it should come to no surprise that all ships have, in their prototype chains, an object which defines the common properties of ships; we can call it the Ship Prototype. The Ship Prototype inherits behaviour from the Entity Prototype, which defines the common properties of all entities (for instance, both ships and planets have a
In order to create an object with a specific prototype from within JavaScript, you use a constructor, which is a function designed to set up a new object in conjunction with the
The same relationships are supposed to apply to Oolite-defined objects, and in trunk they do. For example,
In 1.74 and earlier, most of the type names that should have been constructors instead referred to prototypes, because I was Doing It Wrong. This lead to a bunch of problems which I’ve sort of muddled through, such as compatibility methods for some objects unexpectedly becoming methods of
So, let’s look at the root of the problem, namely the naming of Oolite JavaScript Reference pages. Let’s take
The bulk of the article is divided into three sections, Properties, Methods and Static Methods. All JS reference pages use this structure, although they elide empty sections. The terminology is wrong, and refers to concepts from C++ rather than JavaScript; it’s written that way because that’s how they’re referred to in the SpiderMonkey programming interface and because I started writing the documentation before I fully grokked the language.
In actuality, the Properties section contains non-method properties of the System Prototype – which in 1.75 is
This distinction may be clearer if we look at a type which has more than one instance and also has “static methods”, namely
As I said, the terminology is all wrong, but “fixing” it wouldn’t really make things better. A new Oolite scripter seeing the categories Properties of System.prototype and Properties of System wouldn’t be better off than now. Explaining the distinction in terms specific to each type at the start of the page would be a horrible, mind-damaging thing that would make it much harder for people to actually understand (so please don’t “helpfully” do that). What’s needed is a simple yet basically correct summary of this information, but I’m pathologically incapable of writing it.
As a reward for reading all the way through this short and simplified summary, here’s a fun function you can copy straight into the console. It should work in any version of Oolite which actually has a console:In trunk,
system
with a lowercase s
, except when they shouldn’t, so I thought it would be nice to at least skim through the underlying concepts from an Oolitey perspective.All but the most primitive programming languages have some sort of type system, a way of formalizing the fact that while text, numbers and spaceships are all buckets of bytes to the machine, they mean different things at the higher level of abstraction where the programmer works. Types can be associated with labels, such as variables and properties, or with values; in JavaScript, they’re always associated with values. (This is known as dynamic typing, as opposed to static typing.)
JavaScript almost has a very simple, uniform type system, in which every value is either an object,
null
(representing “no value”), or undefined
(indicating a variable or property does not exist.) In actuality JavaScript’s type system is far more complex for pragmatic reasons, including performance, browser security concerns and, not least, the fact that the entire language was designed and implemented in one week by one guy. But all the things Oolite defines are objects, so we can mostly pretend the simple type system actually exists.In JavaScript, an object is a collection of properties. Each property has an identifier (which can be a string or a number), some attributes specifying things like whether it can be modified, and a value. If the value is a function (or, technically, any callable object), it’s called a method. There is one special rule for methods: if you call a function by referring to it as a property of an object, that object is accessible as
this
from within the function. For example, if you call someObject.method()
, this
will refer to the same object as someObject
while method()
is running. The function itself is not tied to the object, and the same function can be a method of different objects, even under different names.One of the complications that would be nice to gloss over, but is going to be quite important, is that properties can be faked. Normally, you set the value of a given property and it stays set until you change it, but there is an alternative: a property can be based on accessors, where a function is called to get the value (and, optionally, a second function is called to set the value). Historically, it was only possible for host objects – i.e., ones defined by Oolite or the JavaScript engine – to create such properties, but there is a SpiderMonkey extension to do it from scripts and ECMAScript 5th Edition adds a new, standardized way. Host objects mostly do it for performance; for instance, instead of setting the
position
property of each ship’s JavaScript representation every frame, and creating a new JavaScript Vector3D
each time, it’s done on the fly on those occasions where a script actually asks for it.The model as described so far is strictly sufficient, but it does have an important flaw: to create multiple similar objects, it would be necessary to create each object and then set each property for it, even if it’s a default value or an accessor-backed property. Most object-oriented programming systems address this problem using a concept called a class, where each object belongs to a fixed class and each class represents one possible set of properties, and classes can be based on other classes forming an inheritance hierarchy. JavaScript uses a simpler yet more flexible model, in which each object can inherit behaviour from another object, known as its prototype. If you attempt to access a property of an object, but the property does not exist, the prototype is consulted, and if necessary its prototype in turn; this is known as following the prototype chain.
Say
o
is an object with (initially) no properties, and p
is o
’s prototype, also with no properties. If you set p.foo
to 3, and then request o.foo
, you get 3. Further changes to p.foo
are also reflected in o.foo
. If you set o.bar
to 5, then request o.bar
, you get 5, but p.bar
is still not defined.If you set
o.foo
to 7, then o
gets its own foo
property which shadows p
’s. From then on, the two are distinct, unless you delete o.foo
, at which point it again inherits p.foo
.Transferring this to Oolite objects, it should come to no surprise that all ships have, in their prototype chains, an object which defines the common properties of ships; we can call it the Ship Prototype. The Ship Prototype inherits behaviour from the Entity Prototype, which defines the common properties of all entities (for instance, both ships and planets have a
position
property, which is an accessor-backed property of the Entity Prototype).In order to create an object with a specific prototype from within JavaScript, you use a constructor, which is a function designed to set up a new object in conjunction with the
new
operator. In Oolite, you’ll most often use the constructors Vector3D
and Timer
, as in this.v = new Vector3D(1, 0, 0);
. When an object is created using new
, its prototype is set to the value of the constructor’s prototype
property. Note that this is not the constructor’s prototype, but a normal property whose name is prototype
. Here is an example of how you might use this to define your own object hierarchy:
Code: Select all
var p = { foo: 3 };
function P()
{
this.bar = 5;
}
P.prototype = p;
o = new P;
// o is now an object whose prototype is p.
// In ECMAv5 and trunk (even oldjs builds) you can test this with Object.getPrototypeOf(o) == p.
// In earlier versions, you can use o.__proto__ == p, which is a SpiderMonkey extension.
log(o.foo); // 3
log(o.bar); // 5
p.foo = 4;
log(o.foo); // 4
// By the way, the new operator also set o’s “constructor” property to the function P.
Object.getPrototypeOf(player.ship) == PlayerShip.prototype
should be true, and now is. For that matter, new PlayerShip
also “works”, by throwing an exception telling you you’re not allowed to make your own players. If you follow the prototype chain, PlayerShip.prototype
’s prototype is equal to Ship.prototype
, Ship.prototype
’s prototype is Entity.prototype
, Entity.prototype
’s prototype is Object.prototype
and Object.prototype
’s prototype is null
.In 1.74 and earlier, most of the type names that should have been constructors instead referred to prototypes, because I was Doing It Wrong. This lead to a bunch of problems which I’ve sort of muddled through, such as compatibility methods for some objects unexpectedly becoming methods of
Object.prototype
. Another side effect was that you could call methods (or accessor-backed properties) on the prototypes by referring to the purported constructor name. For cases where only one object can exist, like the player ship, the method implementations ignore the this
parameter and use the native Objective-C object directly, which is why you could call PlayerShip.awardCargo()
instead of player.ship.awardCargo()
. (In 1.75, you could instead call PlayerShip.prototype.awardCargo()
, but you’re much less likely to end up in that situation by mistake. Also, it’s very definitely not guaranteed to work in future.)So, let’s look at the root of the problem, namely the naming of Oolite JavaScript Reference pages. Let’s take
System
as an example. The name is System
with a capital S, referring to the name of the constructor – which, to the extent there is such a thing in JavaScript, is the type name. The second sentence of the introduction tells you that there’s one instance of System
, and it’s available as the global variable system
. (A “global variable” is one that’s visible to all code without any special qualification, unless there’s a local variable “shadowing” it. In JavaScript, global variables are actually properties of a “global object”, which is why it says “global property.”)The bulk of the article is divided into three sections, Properties, Methods and Static Methods. All JS reference pages use this structure, although they elide empty sections. The terminology is wrong, and refers to concepts from C++ rather than JavaScript; it’s written that way because that’s how they’re referred to in the SpiderMonkey programming interface and because I started writing the documentation before I fully grokked the language.
In actuality, the Properties section contains non-method properties of the System Prototype – which in 1.75 is
System.prototype
but in earlier versions is accidentally System
itself – and which are inherited by instances, in this case system
. Methods is similar, for properties that happen to be functions. Static Methods contains methods that don’t apply to a particular instance – in this case, functions dealing with other systems – and are attached to the constructor.This distinction may be clearer if we look at a type which has more than one instance and also has “static methods”, namely
Vector3D
. It makes sense to call someEntity.position.add([1, 0, 0])
; this adds the vector (1, 0, 0) to the vector someEntity.position
(following the special rule that arrays can be automatically converted to vectors). It doesn’t make sense to call Vector3D.add([1, 0, 0])
, because Vector3D
doesn’t refer to any specific vector, and in fact it raises an exception, “Vector3D.add is not a function”, because even in earlier versions Vector3D
is the constructor rather than the prototype. (In 1.74, Vector3D.prototype.add([1, 0, 0])
returns undefined
; in trunk, it returns (1, 0, 0). In future versions, it might do some other thing, whatever seems the most efficient non-crashing behaviour.) On the other hand, Vector3D.randomDirectionAndLength()
creates a new vector that isn’t related to any existing vector; it doesn’t make sense to call player.ship.position.randomDirectionAndLength()
.As I said, the terminology is all wrong, but “fixing” it wouldn’t really make things better. A new Oolite scripter seeing the categories Properties of System.prototype and Properties of System wouldn’t be better off than now. Explaining the distinction in terms specific to each type at the start of the page would be a horrible, mind-damaging thing that would make it much harder for people to actually understand (so please don’t “helpfully” do that). What’s needed is a simple yet basically correct summary of this information, but I’m pathologically incapable of writing it.
As a reward for reading all the way through this short and simplified summary, here’s a fun function you can copy straight into the console. It should work in any version of Oolite which actually has a console:
Code: Select all
this.protoChain = function (object)
{
function pr(v)
{
// Get prototype of v, boxing it if it’s a primitive.
if (typeof Object.getPrototypeOf == "function") return Object.getPrototypeOf(new Object(v));
else return v.__proto__;
}
var result = "", first = true;
for (;;)
{
var proto = pr(object);
if (!proto) return result;
if (!first) result += ": ";
else first = false;
result += proto.constructor.name || "<anonymous>";
object = proto;
}
}
protoChain(player.ship)
returns “PlayerShip: Ship: Entity: Object”. In 1.74, you get “Object: Object: Object: Object”, which is distinctly wrong albeit mildly amusing to fans of Catch-22. In either, protoChain(new Vector3D)
returns “Vector3D: Object”, and protoChain([])
returns “Array: Object”. It also deals with (i.e., correctly lies about) primitive values; protoChain(5)
returns “Number: Object”.