JavaScript-DOM Prototypes in Mozilla

Prototype setup on an XPConnect wrapped DOM node in Mozilla

When a DOM node is accessed from JavaScript in Mozilla, the native C++ DOM node is wrapped using XPConnect and the wrapper is exposed to JavaScript as the JavaScript representation of the DOM node. When XPConnect wraps a C++ object it will create a JSObject that is unique to this C++ object. In the case where the C++ object has class info (nsIClassInfo), the JSObject is a more or less empty JSObject which is not really that special. All the methods that are supposed to show up on this JSObject are actually not properties of the object itself, but rather properties of the prototype of the JSObject for the wrapper (unless the C++ object's class info has the flag nsIXPCScriptable::DONT_SHARE_PROTOTYPE set, but lets assume that's not the case here).

As an example of this let's look at an HTML image element in a document.

var obj = document.images[0];

Here, obj will not really have any properties (except for the standard JSObject properties such as constructor, and the non-standard __parent__, __proto__, etc.), all the DOM functionality of obj comes from obj's prototype (obj.__proto__) that XPConnect sets up when exposing the first image in document to JavaScript. Here are a few of the properties of obj's prototype:

obj.__proto__
  parentNode (getter Function)
  src (getter and setter Functions)
  getElementsByTagName (Function)
  TEXT_NODE (Number property, constant)
  ...

All those properties come from the interfaces that the C++ image object (nsHTMLImageElement) implements and chooses to expose to XPConnect through the object's class info. One of these interfaces is nsIDOMHTMLImageElement, others are nsIDOMNSHTMLImageElement (Netscape extensions to the standard interface), nsIDOMEventTarget, nsIDOMEventListener, nsIDOM3Node, and so on.

The prototype object that XPConnect creates for the classes that have class info are shared within a scope (window). Because of this, the following holds true (assuming img1 and img2 are two different image objects in the same document):

img1.__proto__ === img2.__proto__

If img1 would come from one document and img2 from another document, then the above would not be true. Both prototypes would look identical, but they would be two different JSObject's.

This sharing of prototypes lets users do cool things like modify how all instances of a given class works by modifying the prototype of one instance. As an example:

function bar() {
  alert("Hello world!");
}

document.images[0].__proto__.foo = bar;

This would make every image in this document have a callable foo property (i.e. a foo() method).

Alternatively, one can access and modify the prototype of an HTMLImageElement through the prototype property of the constructor:

HTMLImageElement.prototype.foo = bar;

Modifying the prototype of a Host object is not guaranteed by ECMAScript specification. Moreover, no specification guarantees that there will be a globally available HTMLImageElement, or that such object will be the constructor for any arbitrary image's [[Prototype]].

A third way through which one can access the prototype of an object is through the constructor property of the object, which itself points (initially) to the constructor:

document.images[0].constructor.prototype.foo = bar;

Note though, the above may or may not work in Mozilla.

So far so good; we have shared prototypes, and XPConnect gives us most of this automatically. But this is not good enough, in addition to being able to share and represent each "class" with a constructor, we also want users to be able to extend interfaces, like Node. Node is a DOM interface, but there are no pure Node instances; there are lots of different classes that implement Node (HTMLImageElement, HTMLDocument, ProcessingInstruction, et c.).

But the fact that an instance of a Node will never exist in Mozilla does not mean that the Node interface is useless, in fact, Node can be extended just as we've been doing. If you think back to the HTMLImageElement examples above, those examples let you define new properties on all image elements. By modifying the Node top-level object, you can do similar things to all objects that implement Node. This means you can add a property to every node in a DOM tree by doing something like this:

Node.prototype.foo = bar;

Again, modifying host objects is an unsafe practice. It is not guaranteed to work or be error-free in any implementation. It is not standard and cannot be expected to have any result.

Here is an attempt to modify a host object:

   (function(){
       try {
           Image.prototype.src = 1;
       }
       catch(ex){ alert(ex); }
   })();

This demonstrates that the Image constructor, a host object supported in nearly all browsers for Mac and Windows, has a prototype property, and that an attempt to modify the prototype's src - property results in an error.

Another example would be modifying the pageX property of a MouseEvent instance. The pageX property actually needs a patch because it doesn't get set correctly in initMouseEvent bug 411031.

Here is a diagram that shows the prototype layout of a HTMLDivElement in Mozilla:

   HTMLDivElement.prototype
             |
             |.__proto__
             |
    HTMLElement.prototype
             |
             |.__proto__
             |
      Element.prototype
             |
             |.__proto__
             |
       Node.prototype
             |
             |.__proto__
             |
      Object.prototype
             |
             |.__proto__
             |
           null

If you have an instance of a HTMLDivElement in JavaScript, the following will hold true:

div.__proto__ === HTMLDivElement.prototype

which means that the following should also be true:

div.__proto__ === div.constructor.prototype

Non Standard

No browser is required to provide modifiable __proto__, nor a global Node, nor provide any way to get at host objects nor their associated prototypes. If such objects are provided, they are not guaranteed by any specification to have any effect on the environment. Results in other browsers is not guaranteed.

So how does all this work in the Mozilla DOM code?

It all happens in XPConnect and nsDOMClassInfo.{cpp,h} in the DOM code. During startup, the nsDOMClassInfo code registers two different types of "global names", these are names of properties of the global object with special meaning to the DOM code. The two types of "global names" are class constructor names and class prototype names. What's the difference? Class constructor names are names of real classes, and class prototype names are names of "classes" that are inherited by real classes, but are not real classes. A few examples of class constructor names would be HTMLImageElement, HTMLDocument, Element, NodeList, and two examples of class prototype names would be Node and CharacterData.

This registration is done with the nsScriptNameSpaceManager, which is in charge of keeping track of what names are registered in the global namespace, and what kinds of names those names are (i.e. class constructor name, class prototype name). When a class constructor name is registered (nsGlobalNameStruct::eTypeClassConstructor), the nsScriptNameSpaceManager is given a DOM class info ID (a 32 bit ID that identifies class info defined in nsDOMClassInfo). When a class prototype name is registered (nsGlobalNameStruct::eTypeClassProto), the nsScriptNameSpaceManager is given the nsIID of the interface that is inherited by the class which the registered name is a prototype of (e.g. NS_GET_IID(nsIDOMNode) for Node). nsScriptNameSpaceManager also deals with other types of names, but those are unrelated to the DOM object prototype setup, so we will ignore those here.

Once the registration is done, the nsDOMClassInfo code uses the registry every time a named property is resolved on a global object (because of this, the nsScriptNameSpaceManager needs to be pretty fast at looking things up in its registry; that's why it is a hash table). When a property is resolved on the global object, the nsDOMClassInfo code will ask the nsScriptNameSpaceManager if the name is a known name (in nsWindowSH::GlobalResolve()), and if the name is known, the code will look at the type of the name and act accordingly.

If a class constructor or class prototype name is resolved, the class info code will define the constructor for that class, and also define the prototype property of that constructor (i.e. HTMLImageElement.prototype). The prototype of a constructor will either be the prototype object that XPConnect creates for a class (if the name is the name of a real class) or simply an empty JSObject of a specific JSClass that is defined in nsDOMClassInfo.cpp (nsDOMClassInfo::sDOMConstructorProtoClass).

As the prototype property of the constructor is being defined, the code also sets up the prototype of the prototype property of the constructor (i.e. HTMLImageElement.prototype.__proto__). To do this, the code figures out what the name of the immediate prototype of the class is by looking at the parent of the primary interface in the class info (if the name is a class constructor, such as HTMLImageElement) or by looking at the parent of the interface that the IID stored in the nsScriptNameSpaceManager for this name represents (if the name is a class prototype, such as Node). Once the name of the parent interface is known (and the name is not nsISupports) the code will look up a property by that name on the global object. This will cause the code to recurse down along the parent chain of the interface of interest for the name we started out resolving (i.e. nsWindowSH::GlobalResolve() will be called for every name on the parent chain). The result of this recursion is that by resolving the name HTMLImageElement, we'll create the constructor HTMLImageElement, HTMLElement, Element, and Node, and the prototype properties on all those constructor will be correctly set up. This means that the next time the name of a class constructor is resolved in the same scope, say HTMLAnchorElement, the code will resolve the name HTMLAnchorElement, find the parent name, which is HTMLElement, and resolve that, but since we've already resolved HTMLElement as a result of resolving the name HTMLImageElement earlier, the recursion will stop right there.

Ok, so that's how class constructor and their prototype properties are set up, what about the actual prototype chain of a XPConnected DOM object? The beauty of this code is that the prototype property of a class constructor is the real XPConnect prototype for that class. When XPConnect wraps a DOM object (i.e. creates a XPConnect JavaScript wrapper for a DOM object), XPConnect will call the scriptable helper method nsDOMClassInfo::PostCreate() which will make sure the prototype chain of the wrapper JSObject is properly set up. In this call, the nsDOMClassInfo code just needs to resolve the name of the class on the global object, and the prototype will be set up by the resolving code (nsWindowSH::GlobalResolve()). This is also done only once per class, nsDOMClassInfo::PostCreate() checks if the prototype of the prototype of the wrapper JSObject (i.e. obj.__proto__.__proto__) has been set up already, if it has, then there's nothing left to do in nsDOMClassInfo::PostCreate().

Original Document Information