The Surprisingly Elegant Javascript Type Model
Posted: February 21, 2012 Filed under: Javascript Leave a comment »
转自:http://vijayan.ca/blog/2012/02/21/javascript-type-model/
By now, Javascript programmers at large have generally gotten a handle on the prototype-based inheritance schemes that are possible within the language. Still, it seems that many people feel that JS’s idea of types is somewhat lacking. What I want to talk about is the reality that once you dust around the corners a little bit, it’s possible to see that Javascript contains within it the outlines of a reasonably well-defined, somewhat elegant (if not strictly enforced) type model.
I feel that a clear understanding of the nature of the existing informal system would be useful in informing further discussion on the nature of what a more formalized notion of types in Javascript might look like.
Let’s get started.
The Type Model
In idiomatic JS, we can consider three general categories of objects as composing the structural basis of types:
- Type objects – These are just the constructor functions used to instantiate objects.
- Traits objects – This is what I’ll call the objects referred to by the
prototype
field of a constructor function. They provide the method bindings for instances of the type they are associated with. Traits objects are basically the behaviour specification for the instances a type. - Instance objects – These are the actual instances. They are created with their prototype (i.e. delegation target) pointing to the trait object of their type.
Note that these categories are neither exclusive, nor complete. A single Javascript object can belong to more than one category, and there can exist Javascript objects that fall into none of the above categories. But I’ll get to that later. Here’s a diagram showing how these objects are structured:
As noted earlier, Type
(in blue) is the constructor function, Type.prototype
points to the traits object (in green), and new Type()
(in gray) is an instance of Type
. The light-gray dotted line points out the instance-of relationship between new Type()
and Type
. This structure is pretty simple: Types have a field named prototype
that points to its traits object. The traits object has a field named constructor
that points back to the type it is associated with. The instance belongs to the type by virtue of the fact that its prototype (i.e. the object that it delegates to) refers to the traits object for the type.
Pretty straightforward so far, no?
The Subtyping Model
Let’s keep going with that diagram, and model what subtyping looks like in Javascript:
Here, the light-blue dotted line shows the implicit subtype-of relationship between Subtype
andType
. The key point to notice here is the following: Subtype
is a subtype of Type
by virtue of the fact that Subtype.prototype
(the traits object for Subtype
) delegates to Type.prototype
(the traits object for Type
). This is how the delegation-based inheritance in Javascript is leveraged by the type model to enable the behaviour sharing necessary to represent subtype relationships.
This set of relationships is not my construction, but the structure that Javascript already uses, albeit in an informal way, to express type and subtype relationships. To prove this to ourselves, we just need to take a look at how the core Javascript constructor functions are organized. The following diagram shows the actual relationship between three of the core Javascript constructor functions (Object
, Function
, and Array
) and their instances:
Well that got complicated fast, didn’t it? Well, not really. It’s the same simple structure that we were looking at above, except we are also diagramming the fact that functions (and thus constructor functions, and thus the built-in constructor functions Object
, Function
, and Array
themselves) are also instances of Function
.
Yes, this means that Function
is an instance of itself (naturally, since it’s a function, and thus an instance of Function
). This is something we’ve all been dealing with, knowingly or not, for a long time now – all constructor functions are regular functions and thus instances of Function
, and Function
itself is just the constructor function for constructing other functions, so it too is an instance of Function
.
The Pullback
Let’s step back from that rat’s nest of a relationship diagram above. It looks hairy, but in reality it reflects a very simple truth about how Javascript already structures its type relationships for all of its built-in objects. Let’s throw away the instances, the traits objects and all of that stuff, and just look at the types and subtypes. The following is what we see already going on inside the world of Javascript types:
That right there is the type model that’s been present in Javascript for a while now. Pretty neat, huh?
Let’s think about what that picture above tells us. It first tells us that Javascript’s type system and subtyping model is built around two core types: Object
and Function
. The root of all subtype-ofrelationships is Object
, and Function
is the only meta-type in the system. Every type in this system has Function
for its own type, including Function
itself. Function
is basically a universal meta-type, hiding in plain view. Neat, huh?
The Caveats
As cool as observing the above may be, this detailing is more than a bit idealized. While these relationships are there, embedded right into the core Javascript types we use every day, they are notstrict, and they don’t completely cover the language.
Consider, for example, Object.prototype
(or, for that matter, Function.prototype
, orString.prototype
, or any such traits object). In the context of the type system described above, those objects don’t actually HAVE a type of their own. They’re purely structural – they exist only to model the inheritance chain, and provide the method bindings for instances. Despite this, we can easily access them, and pass them around to functions, or return them from functions, or store them in arrays, or anything else you can do with other regular objects which have well-defined types.
For another example, consider the result of the expression Object.create(null)
. This creates a new object that has no prototype that it delegates to. This newly created object is completely un-represented and un-captured by the type-system above.
Furthermore, none of the ‘primitive’ values are covered at all by this system. Primitive strings, numbers, booleans, and the null
value – none are really represented. However, this is ameliorated somewhat by the fact that primitive values (except for null
and undefined
) are auto-boxed when methods are called on them. So, if you squint at them the right way (and refrain from doing things liketypeof
on them), you can basically get away with looking at them as instances of their respective object-constructors (i.e. a primitive string as an “instance” of String
).
All that is a bit.. well.. discomforting. The type system above has some amount of elegance, and it’s already “present” in some sense in the language, but it’s voluntary and does not cover all of the objects we can touch from Javascript code. Is that a good thing or a bad thing? Well, that all depends on your temperament.
Javascript has always been a language with warts, but those warts are a reflection of its history. It’s the ugly duckling that inadvertantly took over the world. It’s the language the dynamic web was built on. It is what it is. However, I understand if you disagree, if this eating-french-fries-off-the-fine-china really gets under your skin. I can feel it a bit too.. tickling the back of my neck.
The Takeaway
I believe that the simple structure used within Javascript to model types contains within it the sketches of a truly powerful system for organizing the behaviour of complex object relationships. Javascript already represents classes, subclasses, and at least one notional metaclass.
Moving on, I think we can use this realization and understanding to start thinking about simple ways in which the existing capabilities can be extended (loosened in certain places, tightened up in others) to allow JS programmers to tap their full power.
More on that when I get a chance to write it up…