Understanding JavaScript, maybe

Discussion and information relevant to creating special missions, new ships, skins etc.

Moderators: another_commander, winston

Post Reply
User avatar
JensAyton
Grand Admiral Emeritus
Grand Admiral Emeritus
Posts: 6657
Joined: Sat Apr 02, 2005 2:43 pm
Location: Sweden
Contact:

Understanding JavaScript, maybe

Post by JensAyton »

I’ve been talking recently about prototype chains and constructors and how everyone should have been spelling 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.
The same relationships are supposed to apply to Oolite-defined objects, and in trunk they do. For example, 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 [EliteWiki] 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 [EliteWiki] 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;
    }
}
In trunk, 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”.
User avatar
Mauiby de Fug
---- E L I T E ----
---- E L I T E ----
Posts: 847
Joined: Tue Sep 07, 2010 2:23 pm

Re: Understanding JavaScript, maybe

Post by Mauiby de Fug »

Congratulations! Even to a semi-drunken squirrel such as I, that actually made sense and cleared up a few things! Until the final bit of code involving "protoChain"... That might have to wait until I'm sober...
User avatar
Cody
Sharp Shooter Spam Assassin
Sharp Shooter Spam Assassin
Posts: 16055
Joined: Sat Jul 04, 2009 9:31 pm
Location: The Lizard's Claw
Contact:

Re: Understanding JavaScript, maybe

Post by Cody »

Mauiby de Fug wrote:
Even to a semi-drunken squirrel such as I, that actually made sense
Semi-drunken squirrels: 1 ~ Stone-cold sober contrabandistas: 0
I would advise stilts for the quagmires, and camels for the snowy hills
And any survivors, their debts I will certainly pay. There's always a way!
User avatar
JensAyton
Grand Admiral Emeritus
Grand Admiral Emeritus
Posts: 6657
Joined: Sat Apr 02, 2005 2:43 pm
Location: Sweden
Contact:

Re: Understanding JavaScript, maybe

Post by JensAyton »

You must have at least this many teeth to understand JavaScript.

Image
User avatar
Smivs
Retired Assassin
Retired Assassin
Posts: 8408
Joined: Tue Feb 09, 2010 11:31 am
Location: Lost in space
Contact:

Re: Understanding JavaScript, maybe

Post by Smivs »

Well, that counts-out E.V. and myself.
Commander Smivs, the friendliest Gourd this side of Riedquat.
User avatar
DaddyHoggy
Intergalactic Spam Assassin
Intergalactic Spam Assassin
Posts: 8501
Joined: Tue Dec 05, 2006 9:43 pm
Location: Newbury, UK
Contact:

Re: Understanding JavaScript, maybe

Post by DaddyHoggy »

Smivs wrote:
Well, that counts-out E.V. and myself.
Alas, I have only a beak... :roll:
Selezen wrote:
Apparently I was having a DaddyHoggy moment.
Oolite Life is now revealed here
User avatar
DaddyHoggy
Intergalactic Spam Assassin
Intergalactic Spam Assassin
Posts: 8501
Joined: Tue Dec 05, 2006 9:43 pm
Location: Newbury, UK
Contact:

Re: Understanding JavaScript, maybe

Post by DaddyHoggy »

Disembodied wrote:
Only a beak, you say?
Well OK, and some claws, and a sword (which I regularly use to split hairs (or indeed hares)) :D
Selezen wrote:
Apparently I was having a DaddyHoggy moment.
Oolite Life is now revealed here
User avatar
Smivs
Retired Assassin
Retired Assassin
Posts: 8408
Joined: Tue Feb 09, 2010 11:31 am
Location: Lost in space
Contact:

Re: Understanding JavaScript, maybe

Post by Smivs »

I think Disembodied might have been ironically commenting on his own lack of any bodily parts. :)
Commander Smivs, the friendliest Gourd this side of Riedquat.
User avatar
DaddyHoggy
Intergalactic Spam Assassin
Intergalactic Spam Assassin
Posts: 8501
Joined: Tue Dec 05, 2006 9:43 pm
Location: Newbury, UK
Contact:

Re: Understanding JavaScript, maybe

Post by DaddyHoggy »

:oops: Erm, well yeh, I guess he could, but I was clearly, ironically, splitting hairs with his statement....
Selezen wrote:
Apparently I was having a DaddyHoggy moment.
Oolite Life is now revealed here
Post Reply