sitelink1 http://msdn.microsoft.com/en-us/library/dd282900(VS.85).aspx 
sitelink2 http://msdn.microsoft.com/en-us/library/dd229916(VS.85).aspx 
sitelink3  
sitelink4  
extra_vars5  
extra_vars6  

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 newoperator). 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.

 

 

Document Object Model Prototypes, Part 2: Accessor (getter/setter) Support

Travis Leithead
Microsoft Corporation

November 1, 2008

Contents

Introduction

This article is the second installment of a two-part series that introduces advanced JavaScript techniques in Windows Internet Explorer 8. This part of the series continues the introduction of Document Object Model (DOM) prototypes in Internet Explorer 8 by describing accessor properties.

An accessor property, also called a getter/setter property is a new type of JavaScript property available in Internet Explorer 8. By using accessor properties, Web developers can create or customize dynamic data-like properties that execute JavaScript code when their values are accessed or modified. The DOM prototype hierarchy introduced in the previous article defines all of its properties as built-in accessors. Web developers can also customize DOM built-in accessors to fine-tune the default behavior of the DOM. This article explains the new accessor property (or getter/setter property) syntax, provides a usage overview, and uses scenarios to demonstrate this property's value.

Two Kinds of Properties: Data and Accessor Properties

It is common practice among Web developers to add custom properties to the Document Object Model (DOM). This existing object extensibility allows added properties to save state, track application status, and so on. Prior to Internet Explorer 8, JavaScript supported only one type of property: one that could store and retrieve a value (ECMAScript 3.1 refers to these as "data properties," while other languages use terms such as "field" and "instance variable" to refer to this concept). In terms of implementation, these existing properties have one "variable slot" that holds a value. Data properties are automatically defined when the assignment operator (=) is used in JavaScript, as shown in this example:

document.data = 5; // Creates a data property named "data"
console.log( document.data ); // Answer: 5 (you guessed it!)

The following figure illustrates the relationship between getter and setter accessors.

Visualizing JavaScript data properties
Figure 1: Visualizing JavaScript data properties

Prior to Internet Explorer 8, JavaScript developers could use only data properties in their code, yet it was clear that some built-in properties in JavaScript and the DOM were not data properties. For example, the built-in DOM property "innerHTML" does much more than simply store a value:

// Access the innerHTML property:
// (gets the sub-element tree as a string)
var str = document.getElementById('element1').innerHTML;
// Assign a string to the innerHTML property:
// (Causes the DOM to parse the string and create a
// new sub-element tree under 'element1')
document.getElementById('element2').innerHTML = str;
// Getting (accessing) and setting (assigning to) yield different behavior:
// (The same API "stringifies" and "parses" depending on if it is read or written to.)

Web developers cannot create similar properties (such as innerHTML) that mimic built-in DOM properties without new functionality in the JavaScript language. This functionality gap widens because many Web developers would like to extend and enhance the built-in properties already available in the DOM. To support this needed behavior, the JavaScript language has added "getter/setter" properties. For the purpose of brevity in this article, I will call getter/setter properties by their ECMAScript 3.1 name of "accessor" properties.

Instead of just storing or retrieving a value, accessor properties call a user-provided function each time they are set or retrieved. Unlike normal properties, accessor properties do not include an implicit "slot" to store a value. Instead, the accessor property itself stores a "getter" (a function that is executed when the property is retrieved) or a "setter" (a function that is executed when the property is assigned a value). The following figure depicts a mental model of an accessor property:

Visualizing JavaScript accessor properties
Figure 2: Visualizing JavaScript accessor properties

Syntax

A special syntax is necessary to define an accessor property because the assignment operator (=) defines a data property by default. Internet Explorer 8 is the first browser to adopt the ECMAScript 3.1 syntax for defining accessor properties:

Object.defineProperty(  [(DOM object) object],
                        [(string)     property name],
                        [(descriptor) property definition] );
All parameters are required.
Return value: the first parameter (object) passed to the function.

A new syntax is also needed to retrieve an accessor property's definition (the getter or setter functions themselves) because simply reading an accessor property will invoke its getter function:

Object.getOwnPropertyDescriptor( [(DOM object) object],
                                 [(string)     property name] );
All parameters are required.
return value: A "property descriptor" object

Note the following restrictions:

  • Both of these new APIs are defined only on the JavaScript global "Object" constructor.

  • The first parameter (the object on which to attach the accessor) supports only DOM instances, interface objects and interface prototype objects in Internet Explorer 8; for more information, see Part 1 of this series. We plan to expand accessor support for custom and built-in JavaScript objects, constructors, and prototypes in a future release.

The following example demonstrates the defineProperty API by defining an accessor called "JSONposition" for an image. The "getter" for this new property converts the image's coordinates into a JSON string. The "setter" reads the JSON string and modifies the image's position accordingly:

// Create a property descriptor object
var posPropDesc = new Object();
// Define the getter
posPropDesc.get = function ()
{
  var coords = new Object();
  coords.x = parseInt(this.currentStyle.left);
  coords.y = parseInt(this.currentStyle.top);
  coords.w = parseInt(this.currentStyle.width);
  coords.h = parseInt(this.currentStyle.height);
  return JSON.stringify(coords);
}
// Define the setter
posPropDesc.set = function (JSONString)
{
  var coords = JSON.parse(JSONString);
  if (coords.x) this.style.left   = coords.x + "px";
  if (coords.y) this.style.top    = coords.y + "px";
  if (coords.w) this.style.width  = coords.w + "px";
  if (coords.h) this.style.height = coords.h + "px";
}
// Define the new accessor property "JSONposition" on a new image
var img = Object.defineProperty(new Image(), "JSONposition", posPropDesc);
img.src = "...";
// Call the new property
img.JSONposition = '{"w":400,"h":100}';
// Read the image's current position
console.log(img.JSONposition);

In this example, defineProperty creates a new accessor property on an image (first parameter) with the name "JSONposition"; the third parameter is an object called a property descriptor that defines the new property's behavior.

Property descriptors are a generic way of describing both the property "type" and its "attributes." As the previous example illustrates, the "getter" and "setter" are two of the possible properties in a property descriptor. Adding either of these properties to a property descriptor will cause defineProperty to create an accessor property. When a getter is not specified, accessing the property value returns the value undefined. Similarly, when a setter is not specified, assigning a value to the accessor does nothing. This is illustrated in the following table:

 

  Getter function only Setter function only Both functions
Property Access ("get") Invoke the getter function Return undefined Invoke the getter function
Property Assignment ("set") No operation Invoke the setter function Invoke the setter function

Table 1: Possible access and assignment results for combinations of getter or setter functions on a JavaScript accessor property

Accessor properties can also be incrementally defined using multiple calls to the defineProperty API. For example, one call to defineProperty might define only a getter. Later, defineProperty might be called again on the same property name to define a setter. At this point, the property has both a getter and setter:

Object.defineProperty(window, "prop",
   { get: function() { return "Can get"; } } );
// ...
Object.defineProperty(window, "prop",
   { set: function(x) { console.log("Can set " + x); } } );
// Now both getter and setter are defined for the property

Similarly, defining either a getter or setter to be undefined essentially unsets whatever getter or setter was previously in place:

Object.defineProperty( document.body, "secondChild",
{
  get: function ()
  {
    return this.firstChild.nextSibling;
  },
  set: function ( element )
  {
    throw new Error("Sorry! This property can't be " +
                    "set. Better luck next time.");
  }
} );
// Changed my mind: don't be so strict about throwing
// an error when setting this property...
Object.defineProperty( document.body, "secondChild",
                       { set: undefined } );

Another keyword that is possible in a property descriptor is the "value" keyword; "value" signals the creation of a data property:

console.log( Object.defineProperty(
   document, "data", { value: 5 } ).data );

This code is exactly equivalent to the data property created in the first code sample in this article. Note that if a property descriptor contains a combination of "value" and "get/set" keywords, then the defineProperty API will return an error.

Property descriptors may also include additional keywords to control the "attributes" of the property. These are reserved for future use; currently Internet Explorer 8 supports only the following attribute keyword values:

 

Property type "Writeable" attribute "Configurable" attribute "Enumeratable" attribute
data property true true true
accessor property N/A true false

Table 2: Valid values for the writable, configurable, and enumerable property descriptor attributes on both a data and accessor property

If you get the combination wrong, the defineProperty API will return an error, as shown in the following code sample.

try
{
  Object.defineProperty(document, "test",
  {
    get: function()
    {
      return 'This is just a test';
    },
    configurable: false;
  } );
}
catch(e)
{
  console.log(e.message);
  // 'configurable' attribute on the property
  // descriptor cannot be set to 'false' on this object
}

To remove an accessor or data property, simply delete it from the object to which it was defined:

delete document.test;
delete document.data;

Accessor Properties and DOM Prototypes

Accessor properties together with the DOM prototype hierarchy complete the scenario of allowing Web developers to have full customization of built-in DOM properties. Accessor properties are the means to customize the DOM's built-in functionality using user-defined JavaScript functionality; the DOM prototype hierarchy is the means for "scoping" the extent of these customizations.

DOM built-in properties are defined on interface prototype objects. As described in Part I, these objects are arranged into a hierarchy; all DOM instances inherit the properties defined at each level in their prototype chain.

One of those built-in properties and a prime target for customization is the innerHTML property.

Prototype chain for a Div instance
Figure 3: Prototype chain for a div instance

With this view of the DOM prototype hierarchy, the Web developer can choose to customize the built-in innerHTML property itself, or override that property on a lower level in this hierarchy. To customize the built-in property, use the defineProperty API, passing in the Element.prototype object as the first parameter, the "innerHTML" string as the second, and the getter or setter functions as part of the property descriptor:

// Customize the built-in innerHTML property
Object.defineProperty(Element.prototype, "innerHTML", /* property descriptor */);

In most cases, the Web developer's objective will not be to simply replace the innerHTML property with new functionality, but rather supplement the existing functionality of innerHTML. In these cases, it is important to be able to invoke the original behavior of innerHTML from within the new getter or setter code. Do this by caching the original accessor's property descriptor (before customizing it):

// Save (cache) the original behavior of innerHTML
var originalInnerHTMLpropDesc = Object.getOwnPropertyDescriptor(Element.prototype, "innerHTML");
// Define my customizations, but use innerHTML when done...
Object.defineProperty(Element.prototype, "innerHTML",
{
  set: function ( htmlContent )
  {
    // TODO: add new innerHTML getter code
    // Call original innerHTML when done...
    originalInnerHTMLpropDesc.set.call(this, htmlContent);
  }
}

At this point, the setter for innerHTML has been customized for all DOM element instances. The getter for innerHTML continues to work as before.

Perhaps the goal of the Web developer is to customize innerHTML for only a subset of element types?only DIV elements, for example. By defining innerHTML at the HTMLDivElement.prototype level, the Web developer overrides the built-in innerHTML property (for DIV element instances only) because the override is found first when JavaScript visits a DIV element's prototype chain:

// Customize innerHTML for DIV Elements only
// (other element types are unaffected)
Object.defineProperty(HTMLDivElement.prototype,
   "innerHTML", /* property descriptor */);

A property override blocks the inheritance of any getter or setter functions from a same-named property at a higher level in the prototype chain. For example, if the property descriptor in the previous sample code contained only the definition for a getter, the setter for this override would be undefined; it would not inherit the setter from higher in the prototype chain.

Finally, when the Web developer wants to apply an override only to a DOM instance, use the defineProperty API with the instance directly:

// Create a div element instance
// with a customized innerHTML property
var div = Object.defineProperty(document.createElement('DIV'),
   "innerHTML",  /* property descriptor */);

As described, accessor properties used together with DOM prototypes create very powerful scenarios. To be most effective, the Web developer should understand the Internet Explorer 8 DOM prototype hierarchy to learn what interface prototype objects define which properties.

Special Case: Deleting Built-in DOM Properties

In the previous section, I concluded by describing how the JavaScript delete operator will remove any accessor or data property. In some cases this may not appear to work. This is because the Internet Explorer 8 interface prototype objects first inherit from internal prototypes (unavailable to JavaScript) that implement internal versions of the same properties available on the "public" prototypes. This implementation detail is evident only when deleting built-in DOM properties from their prototype objects. The prototype hierarchy in the following figure depicts this implementation detail: deleting the innerHTML property from Element.prototype causes the internal innerHTML property to be inherited. This has the appearance of "restoring" (by inheritance) a built-in property to its default state.

Prototype chain for a div instance with implementation-specific internal prototypes
Figure 4: Prototype chain for a div instance with implementation-specific internal prototypes

Powerful Scenarios

To demonstrate the capabilities of accessor properties in conjunction with DOM prototypes, consider two potential scenarios: in the first scenario, a Web page provides a mechanism for a user to make document annotations (such as for online document review and collaboration) and then injects those annotations into the Web page using innerHTML. In this scenario, the Web page has two criteria: the first is ensuring that the injected content is safe by using toStaticHTML; the second is removing certain stylistic elements and attributes of the HTML user-input to prevent layout problems on the page. To simplify this two-step process, the Web page replaces the default functionality of innerHTML with the following custom code (shortened for brevity):

// Save a copy of the built-in property
var innerHTMLdescriptor = Object.getOwnPropertyDescriptor(Element.prototype, 'innerHTML');
// Define the new filter, which makes arbitrary HTML safe and then strips fancy formatting
Object.defineProperty(Element.prototype, 'innerHTML',
  {
    set: function(htmlVal)
      {
        var safeHTML = toStaticHTML(htmlVal);
        // TODO: Code which filters out style attributes + removes stylistic tags from safeHTML
        // Invoke the built-in innerHTML behavior when done.
        innerHTMLdescriptor.set.call(this, safeHTML);
      }
  });

In the second scenario, a framework Web developer defines a new method to make Internet Explorer 8 more cross-browser compatible. Many JavaScript frameworks currently implement custom code to handle cross-browser incompatibilities or to implement abstractions that do the same. In this example, a Web developer adds an addEventListener API to Internet Explorer 8 (addEventListener is part of the W3C DOM L2 Events standard). Note that this example applies the new API at the appropriate places in the DOM prototype hierarchy to prevent the need for a separate abstraction layer of JavaScript code for consumers of the Web developer's framework to learn (code abbreviated for brevity):

// Apply addEventListener to all the prototypes where it should be available.
HTMLDocument.prototype.addEventListener =
Element.prototype.addEventListener =
Window.prototype.addEventListener = function (type, fCallback, capture)
{
  var modtypeForIE = "on" + type;
  if (capture)
  {
    throw new Error("This implementation of addEventListener does not support the capture phase");
  }
  var nodeWithListener = this;
  this.attachEvent(modtypeForIE, function (e) {
    // Add some extensions directly to 'e' (the actual event instance)
    // Create the 'currentTarget' property (read-only)
    Object.defineProperty(e, 'currentTarget', {
      get: function() {
         // 'nodeWithListener' as defined at the time the listener was added.
         return nodeWithListener;
      }
    });
    // Create the 'eventPhase' property (read-only)
    Object.defineProperty(e, 'eventPhase', {
      get: function() {
        return (e.srcElement == nodeWithListener) ? 2 : 3; // "AT_TARGET" = 2, "BUBBLING_PHASE" = 3
      }
    });
    // Create a 'timeStamp' (a read-only Date object)
    var time = new Date(); // The current time when this anonymous function is called.
    Object.defineProperty(e, 'timeStamp', {
      get: function() {
        return time;
      }
    });
    // Call the function handler callback originally provided...
    fCallback.call(nodeWithListener, e); // Re-bases 'this' to be correct for the callback.
  });
}

// Extend Event.prototype with a few of the W3C standard APIs on Event
// Add 'target' object (read-only)
Object.defineProperty(Event.prototype, 'target', {
  get: function() {
    return this.srcElement;
  }
});
// Add 'stopPropagation' and 'preventDefault' methods
Event.prototype.stopPropagation = function () {
  this.cancelBubble = true;
};
Event.prototype.preventDefault = function () {
  this.returnValue = false;
};

Relationship to Standards

Standards are an important factor to ensure browser interoperability for the Web developer. The accessor property syntax has only recently begun standardization. As such, many browsers support an older, legacy syntax:

// Legacy version of Object.defineProperty(document, "test",
// { getter: /*...*/, setter: /*...*/ } );
document.__defineGetter__("test", /* getter function */ );
document.__defineSetter__("test", /* setter function */ );

// Legacy version of Object.getOwnPropertyDescriptor(document, "test");
document.__lookupGetter__("test");
document.__lookupSetter__("test");

One important difference to note in the behavior of __lookupGetter__/__lookupSetter__ is that these APIs visit the prototype chain of the given object to find the getter or setter functions, respectively, while getOwnPropertyDescriptor, as its name implies, checks only the object's "own" properties.

Until other browsers can support the standard accessor property syntax, we recommend using feature-level detection to handle browser interoperability issues (including checking the Internet Explorer 8 restriction of DOM objects only):

if (Object.defineProperty)
{
  // Use the standards-based syntax
  var DOMonly = false;
  try
  {
    Object.defineProperty(new Object(), "test", {get:function(){return true;}});
  }
  catch(e)
  {
    DOMonly = true;
  }
}
else if (document.__defineGetter__)
{
  // Use the legacy syntax
}
else
{
  //neither defineProperty or __defineGetter__ supported
}

Restricted Properties

Some built-in DOM properties provide important information to Web applications that help them make security decisions, gather analytics, or provide customized functionality. For these reasons, the following properties cannot be replaced by using Object.defineProperty:

  • location.hash
  • location.host
  • location.hostname
  • location.href
  • location.search
  • document.domain
  • document.referrer
  • document.URL
  • navigator.userAgent
  • [properties of window]

Summary

Accessor properties (also known as getter/setter properties), give Web developers the power to create and customize built-in properties available in the DOM. This article has introduced the accessor property syntax, provided an overview of how to use these properties, and shown how they work together with the DOM prototype hierarchy to complete many Web developers' scenarios.

번호 제목 글쓴이 날짜 조회 수
125 characters from ISO 8859-1 황제낙엽 2020.11.10 28619
124 [ActiveX] CAB파일 수동 설치(레지스트리 등록) 방법 황제낙엽 2017.03.16 3162
123 네이버의 무료 나눔 글꼴 황제낙엽 2020.05.06 1110
122 <img> image 엘리먼트에서 이미지를 base64로 인코딩해서 사용하기 file 황제낙엽 2017.04.01 977
121 Document documentMode Property file 황제낙엽 2011.10.04 906
120 encoding, charset, code page, UTF-8, UNICODE ... file 황제낙엽 2013.08.07 731
119 pt, px, em, % 비교표 file 황제낙엽 2011.05.24 731
» [MSDN] Document Object Model Prototypes (IE8) 황제낙엽 2011.03.24 716
117 User Agent 에 관련된 링크 황제낙엽 2017.11.20 595
116 Canvas 곡선 그리기 file 황제낙엽 2016.08.22 424
115 document.domain (from mozilla.org) 황제낙엽 2013.03.13 407
114 ASCII Table and Description file 황제낙엽 2011.08.10 357
113 DIV태그로 테이블 만들기 황제낙엽 2005.12.24 300
112 Object의 주요 속성 황제낙엽 2011.02.14 278
111 모바일 브라우저에서 iframe 의 스크롤 문제 황제낙엽 2012.01.12 267
110 Canvas 도형의 클릭 이벤트 처리 황제낙엽 2016.08.22 263
109 Style cssText Property 황제낙엽 2012.09.13 257
108 스타일-보더 테스트 관련 레퍼런스 황제낙엽 2013.01.04 248
107 HTML5 강좌 2강 - HTML5 시맨틱웹을 위한 구성요소 file 황제낙엽 2016.12.03 246
106 로드밸런싱(L4)+아파치를 운영시 etag제거로 캐시 성능 최적화 file 황제낙엽 2018.03.28 226