JavaScript-DOM Prototypes in Mozilla
From MDC
Contents |
[edit] 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 function:
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 property 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's prototype property:
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 (except the constructor functions on the global
object). But this is not good enough, in addition to being able to
share and represent each "class" with a constructor function, we also
want users to be able to extend interfaces, like for instance
Node. Node is a DOM interface, but there is
no Node class, there are lots of different classes that implement Node
(HTMLImageElement, HTMLDocument, ProcessingInstruction, et c.), but an
instance of Node will never exist in Mozilla.
But the fact that an
instance of a Node will never exist in Mozilla does not mean that the
Node interface is useless, the Node class is indeed very useful since it
gives the user even more flexibility when working with the Mozilla
DOM. If you think back to the HTMLImageElement examples above, those
examples let you define new properties on all image elements. By using
the Node constructor function 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); }
})();
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
and the following should also be true, but it's not yet so in Mozilla:
div.constructor === HTMLDivElement
which means that the following should also be true:
div.__proto__ === div.constructor.prototype
[edit] Non Standard
No browser is required to provide modifiable __proto__, nor a global Node, nor provide an internally modifiable constructor (and associated prototype) for Host objects. 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.
[edit] 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 function for that class, and
also define the prototype property of that constructor function
(i.e. HTMLImageElement.prototype). The prototype of a constructor
function 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 function is being
defined, the code also sets up the prototype of the prototype property
of the constructor function
(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 functions
HTMLImageElement, HTMLElement, Element, and Node, and the prototype
properties on all those constructor functions 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 functions 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().
[edit] Original Document Information
- Author(s): Fabian Guisset
- Last Updated Date: February 2, 2002
- Copyright Information: Copyright (C) Fabian Guisset