Jumat, 29 November 2024

Document Object Model Prototypes, Part 1: Introduction

 

Document Object Model Prototypes, Part 1: Introduction

Travis Leithead
Microsoft Corporation

November 1, 2008

Contents

Introduction

This article is the first installment of a two-part series that introduces advanced JavaScript techniques in Windows Internet Explorer 8.

Web applications have come a long way since the birth of the static Web page. Today, Web developers need improved programming functionality, flexibility, and features to enable them to build the next generation of Web applications. The Internet Explorer Web platform provides many of the features and functionality necessary to build those applications. Where the Web platform's built-in support ends, JavaScript, the principle scripting language used on the Web, is often used to code innovative new features that supplement the Web platform, cater to Web site specific scenarios, normalize differences between browsers, and so on.

To further empower Web developers with the programming tools necessary to build new JavaScript scenarios that innovate, extend, and build-upon the Web platform, Internet Explorer 8 offers a collection of features that extend some of JavaScript's advanced functionality into the Document Object Model (DOM). This article provides an overview of JavaScript prototype inheritance and introduces the DOM prototypes feature available in Internet Explorer 8; Part 2 introduces a new type of JavaScript property called an accessor property (or getter/setter property).

Prototypes in JavaScript

To begin a discussion of DOM prototypes, it is crucial to understand the JavaScript notion of "prototypes." Simply put, a prototype is like a class object in other languages—it defines the properties shared by all instances of that class. However, unlike a class, a prototype can be retrieved and changed at runtime. New properties can be added to a prototype or existing properties can be removed. All changes are reflected instantaneously in objects that derive from that prototype. How does this work? JavaScript is a dynamic language; rather than compiling the properties that exist on prototypes into static tables prior to execution, JavaScript must dynamically search for properties each time they are requested. For example, consider a basic inheritance scenario where a prototype "A.prototype" inherits from another prototype "B.prototype" and object "a" is an instance of prototype "A.prototype". If a property is requested on the instance object "a", then JavaScript performs the following search:

  1. JavaScript first checks object "a" to see if the property exists on that object. It does not; therefore, JavaScript goes to Step 2.

  2. JavaScript then visits "A.prototype" (the prototype of object "a") and looks for the property. It still doesn't find it so JavaScript goes on Step 3.

  3. JavaScript finally checks "B.prototype" (the prototype of "A") and finds the property. This process of visiting each object's prototype continues until JavaScript reaches the root prototype. This series of links by prototype is called the "prototype chain".

Given the following code:

console.log( a.property );

The "property" property does not exist on the object "a" directly, but because JavaScript checks the prototype chain, it will locate "property" if it is defined somewhere in the chain (on "B.prototype" for example, as shown in the following figure).

A Prototype Chain
Figure 1: A Prototype Chain

In JavaScript, an object's prototype cannot be directly accessed programmatically as drawn in the previous figure. The "links" from object "a" to prototype "A" to prototype "B" are generally hidden from the developer and only maintained internally by the JavaScript engine (some implementations may reveal this as a proprietary property). In this article, I will call this link the "private prototype", or use the syntax "[[prototype]]". For the programmer, JavaScript exposes prototype objects through a "constructor" object with a "prototype" property, as shown in the following figure. The constructor object's name is like the name of a class in other programming languages, and in many cases is also used to "construct" (create) instances of that object through the JavaScript new operator. In fact, whenever a JavaScript programmer defines a function:

function A() { /* Define constructor behavior here */ }

Two objects are created: a constructor object (named "A"), and an anonymous prototype object associated with that constructor ("A.prototype"). When creating an instance of that function:

var a = new A();

JavaScript creates a permanent link between the instance object ("a") and the constructor's prototype ("A.prototype"). This is the "private prototype" illustrated in the previous figure.

The prototype relationship exists for all of JavaScript's objects, including built-in objects. For example, JavaScript provides a built-in "Array" object. "Array" is the name of the constructor object. The "Array" constructor object's "prototype" property is an object that defines properties that are "inherited" by all Array instances (because the instance objects will include that prototype in their prototype chain). The Array.prototype object may also be used to customize or extend the built-in properties of any Array instances (e.g., the "push" property) as I will describe shortly. The following code illustrates the arrangement of constructor, prototype, and instance objects for "Array":

var a = new Array(); // Create an Array instance 'a'
a.push('x'); // Calls the 'push' method of Array's prototype object

The following figure illustrates these relationships.

The relationship between an Array instance, its constructor, and its prototype.
Figure 2: The relationship between an Array instance, its constructor, and its prototype

For each Array instance, when a property is requested (like "push"), JavaScript first checks the instance object to see if it has a property called "push"; in this example it does not, so JavaScript checks the prototype chain and finds the property on the Array.prototype object.

Web developers can add, replace, or "shadow" (override by causing a property to be found earlier in the prototype chain) any of the built-in properties in JavaScript because all built-in constructors have prototypes that are fully accessible. For example, to modify the behavior of the "push" built-in property for all Array instances, the developer simply needs to replace the "push" property in one location, the Array's prototype object:

Array.prototype.push = function () { /* Replaced functionality here */ };

At this point, all instances of Array will use the replaced functionality by default. To create a special case and specify alternate behavior for specific instances, define "push" locally on the instance to "shadow" the default "push" behavior:

a.push = function() { /* Specific override for instance "a" */ };

In some scenarios, the Web developer may not explicitly know the name of the constructor object for a given instance, yet may want to reach that instance's prototype. For this reason, JavaScript provides a "constructor" property that points to the constructor object used to create it. The following two lines of code are semantically equivalent:

Array.prototype.push         = function () { /* Custom behavior here */ };
a.constructor.prototype.push = function () { /* Custom behavior here */ };

The capability to add to and modify prototypes is very powerful.

I like to think of the relationship between constructor, prototype, and instance as a triangle. Instances point to constructors via the "constructor" property. Constructors point to prototype via the "prototype" property. Prototypes are linked to instances via the internal [[prototype]], as shown in the following figure.

The relationship between constructors, prototypes, and instances.
Figure 3: The relationship between constructors, prototypes, and instances.

DOM Prototypes

The prototype behavior just described has existed in Internet Explorer for a long time. For the first time. However, Internet Explorer 8 extends this semantic capability into the Document Object Model (DOM). When Web developers want to interact with a Web page through JavaScript, they must use and interact with DOM objects, which are not part of the core JavaScript language. In previous versions of Internet Explorer, the DOM only provided object "instances" to the JavaScript programmer. For example, the DOM property createElement creates and returns an element instance:

var div = document.createElement('DIV'); // Returns a new instance of a DIV element

This 'div' instance (much like the Array instance 'a') is derived from a prototype that defines all of the properties that are available to that instance. Prior to Internet Explorer 8, the JavaScript constructor and prototype objects for 'div' instances (and all other instances), were not available to the Web developer. Internet Explorer 8 (when running in document mode: IE8) reveals the constructor and prototype objects to JavaScript—the same objects used internally in this and previous versions of the browser. In addition to allowing the customization of DOM object instances by way of their prototypes as described in the previous section, it also helps to clarify the internal representation of the DOM and its unique hierarchy in Internet Explorer (unique compared to other browsers).

Terminology

DOM prototypes and constructors are similar but not identical to the built-in JavaScript prototypes and constructors as previously described. To help clarify these differences, I will refer to the DOM analog of a JavaScript constructor object as an "interface object"; prototype objects in the DOM I will call "interface prototype objects," and I will call instance objects in the DOM "DOM instances." For comparison, let me reuse the triangle relationship diagram I presented earlier for JavaScript prototypes. DOM instances point to interface objects via the "constructor" property. Interface objects point to interface prototype objects via the "prototype" property. Interface prototype objects are linked to DOM instances through the same internal [[prototype]].

The relationship between interface objects, interface prototype objects, and DOM instances.
Figure 4: The relationship between interface objects, interface prototype objects, and DOM instances.

With Internet Explorer 8 running in IE8 standards mode, each DOM instance (such as window, document, event, and so on) has a corresponding interface object and interface prototype object. However, these objects differ from their JavaScript analogs in the following ways:

  • Interface objects

    • Interface objects generally do not have "constructor" functionality (cannot create a new DOM instance by using the JavaScript new operator). Exceptions include a few interface objects shipped in previous versions of Internet Explorer:

      • Option (alias for HTMLOptionElement)
      • Image (alias for HTMLImageElement)
      • XMLHttpRequest
      • XDomainRequest, which is new to Internet Explorer 8
    • The "prototype" property of interface objects may not be replaced (changed). An interface object's prototype property cannot be assigned a different interface prototype object at run-time.

  • Interface prototype objects

    • Interface prototype objects define the properties available to all DOM instances, but these built-in properties cannot be permanently replaced (e.g., the JavaScript delete operator will not remove built-in properties).

Other subtle but important differences are called out near the end of this article.

DOM Prototype Inheritance

As suggested by the W3C DOM specs and formalized by the W3C WebIDL draft standard (as of this writing), interface objects that describe the DOM are organized hierarchically in an inheritance tree by using prototypes. This tree structure conceptually places common characteristics of HTML/XML documents into the most generic type of interface prototype object (such as "Node"), and introduces more specialized characteristics into interface prototype objects that "extend" (via prototypes) that basic functionality. DOM instances returned by various DOM operations (such as createElement) will have a constructor property that generally refers to an interface objects at a leaf-node in this hierarchy. The following figure illustrates part of the DOM hierarchy as specified by the W3C DOM L1 Core specification; this represents only a fraction of the interface objects supported by Web browsers (many of which are not yet specified by W3C standards).

The DOM Hierarchy specificed by the W3C DOM L1 Core Specification.
Figure 5: The DOM Hierarchy Specificed by the W3C DOM L1 Core Specification.

Internet Explorer 8 reveals a DOM prototype hierarchy that is dramatically simpler than the arrangement shown above; primarily because Internet Explorer's object model predates the standardization of the DOM hierarchy as shown. We opted to present the object model to Web developers as-is rather than put up a veneer that gives the appearance of conforming to the DOM hierarchy exactly. This way, we can better articulate and prepare Web developers for future standards-compliance changes to the DOM. The following figure illustrates the unique DOM prototype hierarchy of Internet Explorer 8 using only the interface objects included in the previous illustration. Again, this represents only a fraction of the interface objects supported by Internet Explorer 8:

Partial DOM Hierarchy Supported By Internet Explorer 8.
Figure 6: Partial DOM Hierarchy Supported By Internet Explorer 8.

Note that the common ancestor "Node" is missing. Also note how comments "inherit" from Element (this makes sense when you consider that Internet Explorer still supports the deprecated "<comment>" element).

Customizing the DOM

With an understanding of the Internet Explorer 8 DOM prototype hierarchy, Web developers can begin to explore the power of this feature. To get your creative juices flowing, I will present two real-world examples. In this first example, a Web developer wants to supplement the Internet Explorer 8 DOM with a feature from HTML5 that is not yet available (as of this writing). The HTML5 draft's getElementsByClassName is a convenient way to find an element with a specific CSS class defined. The following short code example implements this functionality by leveraging the Selectors API (new to Internet Explorer 8) and the HTMLDocument and Element interface prototype objects:

function _MS_HTML5_getElementsByClassName(classList)
{
  var tokens = classList.split(" ");
  // Pre-fill the list with the results of the first token search.
  var staticNodeList = this.querySelectorAll("." + tokens[0]);
  // Start the iterator at 1 because the first match is already collected.
  for (var i = 1; i < tokens.length; i++)
  {
    // Search for each token independently
    var tempList = this.querySelectorAll("." + tokens[i]);
    // Collects the "keepers" between loop iterations
    var resultList = new Array();
    for (var finalIter = 0; finalIter < staticNodeList.length; finalIter++)
    {
      var found = false;
      for (var tempIter = 0; tempIter < tempList.length; tempIter++)
      {
        if (staticNodeList[finalIter] == tempList[tempIter])
        {
          found = true;
          break; // Early termination if found
        }
      }
      if (found)
      {
        // This element was in both lists, it should be perpetuated
        // into the next round of token checking...
        resultList.push(staticNodeList[finalIter]);
      }
    }
    staticNodeList = resultList; // Copy the AND results for the next token
  }
  return staticNodeList;
}
HTMLDocument.prototype.getElementsByClassName = _MS_HTML5_getElementsByClassName;
Element.prototype.getElementsByClassName = _MS_HTML5_getElementsByClassName;

Aside from being light on the parameter verification and error handling, I think the ease by which the Internet Explorer 8 DOM has been extended is clear.

In a second example, a Web developer wants to write a function to fix legacy script that has not yet been updated to support Internet Explorer 8. During development of Internet Explorer 8, the setAttribute/getAttribute APIs were fixed to correctly process the 'class' attribute name (among other fixes). Many scripts today use custom code to handle this legacy bug in previous versions of Internet Explorer. The following script catches any instances of this legacy code and fixes it up dynamically. Note that the Web developer leverages the Element interface object to change the behavior for getAttribute and setAttribute on every element (because setAttribute and getAttribute are defined on Element's interface prototype object):

var oldSetAttribute = Element.prototype.setAttribute;
var oldGetAttribute = Element.prototype.getAttribute;
// Apply the change to the Element prototype...
Element.prototype.setAttribute = function (attr, value)
  {
    if (attr.toLowerCase() == 'classname')
    {
      // Older scripts expect 'className' to work, don't
      // disappoint them. Avoiding creating a 'className'
      // attribute in IE8 standards mode
      attr = 'class';
    }
    // TODO: Add other fix-up here (such as 'style')
    oldSetAttribute.call(this, attr, value);
  };
Element.prototype.getAttribute = function (attr)
  {
    if (attr.toLowerCase() == 'classname')
    {
      return oldGetAttribute.call(this, 'class');
    }
    // TODO: Add other fix-up here (e.g., 'style')
    return oldGetAttribute.call(this, attr);
  };

When legacy script runs after this code, all calls to setAttribute or getAttribute (from any element) will have this fix-up applied.

Additional JavaScript/DOM integration improvements

To further round out the scenarios in which DOM prototypes might be used, we addressed several cross-browser interoperability issues regarding Internet Explorer's JavaScript/DOM interaction:

  • Handling of the JavaScript delete operator

  • Support for call and apply on DOM functions

Delete, the New "Property Undo Mechanism"

Internet Explorer 8 now supports JavaScript's delete operator to remove (or undo) properties dynamically added to DOM instances, interface objects, or interface prototype objects. In previous versions of Internet Explorer, removing any dynamic property from a DOM instance with the delete operator caused a script error; Internet Explorer 8 will now properly removes the dynamic property:

document.newSimpleProperty = "simple";
console.log(document.newSimpleProperty); // Expected: "simple"
try
{
  delete document.newSimpleProperty; // Script error in Internet Explorer 7
}
catch(e)
{
  console.log("delete failed w/error: " + e.message);
  document.newSimpleProperty = undefined; // Workaround for older versions
}
console.log(document.newSimpleProperty);  // Expected: undefined

Delete also plays an important role for built-in DOM properties and methods that are overwritten by user-defined objects; in these cases delete removes the user-defined object, but does not remove the built-in property or method:

var originalFunction = document.getElementById; // Cache a copy for IE 7
document.getElementById = function ()
  {
    console.log("my function");
  };
document.getElementById(); // This call now invokes the custom function
try
{
  delete document.getElementById; // Script error in IE 7
}
catch(e)
{
  console.log("delete failed w/error: " + e.message);
  document.getElementById = originalFunction; // Workaround for IE 7
}
console.log(document.getElementById); // Expected: getElementById function

Calling Cached Functions

One interoperability issue with calling cached functions is now fixed in Internet Explorer 8 (a cached function is a DOM function object that is saved to a variable for later use). Consider the following JavaScript:

var $ = document.getElementById;
var element = $.call(document, 'id');

Conceptually, when the function "getElementById" is cached to "$" it could be considered officially severed from the object that originally "owned" it ("document" in this case). To correctly invoke a "severed" (cached) property, JavaScript requires the Web developer to be explicit in specifying the scope; for cached DOM properties, this is done by providing a DOM instance object as the first parameter to JavaScript's "call" or "apply" properties as shown in the previous code sample. For interoperability, we recommend the use of call/apply when invoking cached functions from any object.

With the introduction of DOM prototypes, it is also possible to cache properties defined on interface prototype objects, as illustrated in the example code for the getAttribute/setAttribute fix-up scenario previously described. Use of call or apply is required in these scenarios because function definitions on interface prototype objects have no implicit DOM interface scope.

Internet Explorer 8 continues to support a limited technique of invoking cached functions (primarily for backwards compatibility):

var $ = document.getElementById; // Cache this function to the variable '$'
var element = $('id');

Note that neither call nor apply is used; Internet Explorer "remembers" the object to which the function belonged and implicitly calls "$" from the proper scope (the document object). The preceding code is not recommended, as it will work only with Internet Explorer; also be aware that DOM functions cached from an interface prototype object in Internet Explorer 8 cannot be invoked by using this technique.

Known Interoperability Issues

The following are known interoperability issues with the Internet Explorer 8 implementation of DOM prototypes. We hope to address these issues in a future release; note these should not significantly influence core scenarios enabled by DOM prototypes.

Interface Objects

  • Constant properties (e.g., XMLHttpRequest.DONE) are not supported on any interface objects.

  • Interface objects do not include Object.prototype in their prototype chain.

Interface Prototype Objects

  • Interface prototype objects do not include Object.prototype in their prototype chain.

  • DOM instances and prototype objects currently only support the following properties from Object.prototype: "constructor", "toString", and "isPrototypeOf".

Functions

  • Built-in DOM properties that are functions (such as functions on interface prototype objects) do not include Function.prototype in their prototype chain.

  • Built-in DOM properties that are functions do not support callee, caller, or length.

Typeof

  • The native JavaScript operator typeof is not properly supported for built-in DOM properties that are functions; the return value for typeof that would otherwise return "function" returns "object".

Enumeration

  • Interface prototype objects include support for enumeration of their "own" built-in properties that are functions (a new feature of Internet Explorer 8). DOM instances include support for enumeration of all properties available in their prototype chain except properties that are functions (legacy Internet Explorer 7 behavior). For interoperability, to obtain a full enumeration of all properties available on a DOM instance we suggest the following code:

    function DOMEnumProxy(element)
    {
      var cons;
      for (x in element)
      {
        // x gets all APIs exposed on object less the
        // methods (IE7 parity)
        this[x] = 1;
      }
      try { cons = element.constructor; }
      catch(e) { return; }
      while (cons)
      {
        for (y in cons.prototype)
        {
          // y gets all the properties from the next level(s) in the prototype chain
          this[y] = 1;
        }
        try
        {
          // Avoid infinite loop (e.g., if a String instance parameter is passed)
          if (cons == cons.prototype.constructor)
            return;
          cons = cons.prototype.constructor;
        }
        catch(e) { return; }
      }
    }

    This function constructs a simple proxy object with full property enumeration that is interoperable with the enumeration of other DOM prototype implementations:

    m = new DOMEnumProxy(document.createElement('div'));
    for (x in m)
        console.log(x);

In summary, DOM prototypes are a powerful extension of the JavaScript prototype model. They provide Web developers with the power and flexibility to create scenarios that innovate, extend, and build-upon Internet Explorer 8’s Web platform. Part 2 of this article introduces the getter/setter syntax supported on DOM objects.

Tidak ada komentar: