Transitioning Existing Code to the ES5 Getter/Setter APIs

In my recent blog post, Chakra: Interoperability Means More Than Just Standards, I explained why IE9 only supports the ECMAScript 5 API for defining getter/setter methods. I also mentioned that it is fairly trivial to define a simple compatibility library to help transition existing code to this new API. This is part of what it means to support the same markup using feature detection, not browser detection, so you get the same results in different browsers. In this post I‘ll show you the code for such a library.

Using the non-standardized legacy getter/setter API supported by some browsers you normally define a getter/setter property in a manner such as this:

 myObject.__defineGetter__("p", function() {/* getter function body */});
myObject.__defineSetter__("p", function(v) {/* setter function body */});

Using the standard ES5 API, the equivalent definitions look like this:

 Object.defineProperty(myObject,"p",
   {get: function() {/* getter function body */}}
);
Object.defineProperty(myObject,"p",
   {set: function(v) {/* setter function body */}}
);

As you can see, each __defineGetter__ or __defineSetter__ method call is mapped to an equivalent call to the ES5 Object.defineProperty function. If you have existing code that contains many such calls that you need to work in IE9 or any other ES5 complaint browser, you can avoid a lot of editing and automate this mapping process. You can do this by creating definitions of __defineGetter__ and __defineSetter__ that use defineProperty to create the getter/setter properties and include these definitions in your code. Here is what you need:

 //emulate legacy getter/setter API using ES5 APIs
try {
   if (!Object.prototype.__defineGetter__ &&
        Object.defineProperty({},"x",{get: function(){return true}}).x) {
      Object.defineProperty(Object.prototype, "__defineGetter__",
         {enumerable: false, configurable: true,
          value: function(name,func)
             {Object.defineProperty(this,name,
                 {get:func,enumerable: true,configurable: true});
      }});
      Object.defineProperty(Object.prototype, "__defineSetter__",
         {enumerable: false, configurable: true,
          value: function(name,func)
             {Object.defineProperty(this,name,
                 {set:func,enumerable: true,configurable: true});
      }});
   }
} catch(defPropException) {/*Do nothing if an exception occurs*/};

The if statement does feature detection to determine if the compatibility definitions are necessary. There are two parts to this determination. First it checks whether __defineGetter__ is already available. If it isn’t, it then checks that defineProperty is available and that it supports creating getter properties. It does this by trying to use defineProperty to actually define a getter property named x for a new object and then trying to access that property. If defineProperty isn’t available the attempt to call it will raise an exception which is caught by the try-catch statement that surrounds the if statement. If defineProperty is available but it does support the creation of getter/setter properties on normal objects the call will either throw an exception or the access to the value of x will return undefined, which is a false value. This testing of both the existence and operation of defineProperty is necessary because IE8 includes defineProperty but only supports its use with DOM objects. This is an example of a situation that illustrates that browser feature detection sometimes needs to carefully probe for the desired functionality. Simple checking for the existence of an object or method is not always sufficient.

If these conditions are met then it is possible to emulate the legacy API. The two calls to defineProperty in the body of the if define the actual legacy API methods. The bodies of these methods, when called also use defineProperty to create getter or setter properties.

This code should be inserted at the beginning of an application before any calls to __defineGetter__ or __defineSetter__ are made. In practice, you may want to insert this code into a separate script file that you load before any other code.

With this compatibility code included, your application that uses __defineGetter__ or __defineSetter__ should work on any browser that supports getter/setter properties. If a browser only supports the new ES5 API, the compatibility methods are automatically defined and used. If a browser only supports the legacy API or if it supports both the legacy and ES5 APIs the compatibility methods are not necessary, and the built-in versions of __defineGetter__ or __defineSetter__ are used.

What if you are writing new code that needs to define getter/setter properties? You probably will want that code to run in both new ES5-based browsers and in older browsers that only support the legacy API? You could use this same compatibility package and use the legacy API to define the getter/setter properties. However, it is more forward compatible to write new code using the standard ES5 APIs. You can do this by creating a different compatibility package that uses the legacy API to emulate the ES5 API. Here is the code:

 //emulate ES5 getter/setter API using legacy APIs
if (Object.prototype.__defineGetter__&&!Object.defineProperty) {
   Object.defineProperty=function(obj,prop,desc) {
      if ("get" in desc) obj.__defineGetter__(prop,desc.get);
      if ("set" in desc) obj.__defineSetter__(prop,desc.set);
   }
}

The if statement is again a feature detection check. This time it makes sure that the old API is present and that the ES5 API is missing. If this is the case, it defines the Object.defineProperty function to detect any uses that are trying to define either a getter or a setter or both and then uses the legacy API to actually define them. Note that this is only a partial implementation of the defineProperty functionality. ES5’s defineProperty function can be used to perform other forms of property definition or redefinition in addition to defining getter/setter properties. Many of these new capabilities cannot be easily emulated using common legacy APIs so this compatibility version of the function does not attempt to do so. It just supports getter/setter property definitions.

We all want more capable browsers that enable more compelling web sites. Sometimes new browser functionality introduces dilemmas for web developers who want the same markup to produce the same results across browsers. Developers may ask; do you use the new functionality with the result that your application will not work on older browsers, or do you simply ignore the new functionality? Simple compatibility packages like the ones described here are a pragmatic way to deal with these situations and get the best of both worlds.

Allen Wirfs-Brock

Microsoft JavaScript Language Architect

Edit 9/7 - fixed type in third paragraph

Comments

  • Anonymous
    September 07, 2010
    thanks for an excellent post.

  • Anonymous
    September 07, 2010
    The comment has been removed

  • Anonymous
    September 07, 2010
    The comment has been removed

  • Anonymous
    September 07, 2010
    Looking forward to the IE9 Beta with fixed innerHTML! Being able to use the "same markup" and "same script" in IE as well as I can in all the other browsers will be awesome! Will there be a post on the blog regarding all the fixes to innerHTML that have been added?

  • Anonymous
    September 07, 2010
    So this is great to support custom getters/setters in IE9 and other browsers but what about IE8, IE7, and IE6? As much as I'd love to drop support for all of them right now I can't which means that for IE users on XP there will never be support for getters/setters. Will the Google ChromeFrame solve this issue for IE users?

  • Anonymous
    September 07, 2010
    "Transitioning" is not a real word. Please don't invent words when there are simpler existing ones.

  • Anonymous
    September 07, 2010
    Steve, please don't make snarky comments before using a dictionary. Transitioning is both a word (in every dictionary), and fairly common one (8.6M+ hits in a search engine).

  • Anonymous
    September 07, 2010
    @Meni Please link to me where in the HTML5 standard where WebGL is included.

  • Anonymous
    September 07, 2010
    @Meni If you want WebGL then you go to W3C and ask them to add 3D capabilities to the W3C webstandards (3D Canvas and/or 3D SVG) and stop complaining about it here.

  • Anonymous
    September 07, 2010
    @meni >> WebGL is not a standard. Period.

  • Anonymous
    September 08, 2010
    I admit to being rather confused that your "the equivalent definitions look like this" code block gets it wrong by defining non-enumerable, non-configurable accessor properties, but then the block a few further down gets this right.  I understand the desire for brevity in initial explication, but why muddy the waters like that?

  • Anonymous
    September 08, 2010
    will Internet explorer 9 use less memory and less CPU than internet explorer 8?

  • Anonymous
    September 08, 2010
    @Jeff Walden Jeff, of course you are correct that there isn't a 100% equivalence in the first two examples.  However,  since there is no offical spec. for the legacy API and some semantic details actually differ among its browser implementations, it is hard to know with what we would be trying to be 100% equivalent.  Regardless, as you infer I was just trying to not keep the article simple.  In reality, I suspect that most legacy uses of getter/setter properties don't really care about the enumerability or configurability of the properties.  Finally, there are subtle differences in the semantics of legacy pre-ES5 getter/setter properties and ES5 accessor properties.  The goal of bridge routines such as these is to support interoperability of common use cases rather than providing a perfect bi-directional emulation of every semantic detail. Allen

  • Anonymous
    September 08, 2010
    @Meni Since you ask, I would classify this post as being more about interoperability than about standards. I have no problems using the word "Standands". In fact,  I use it every day. But as I discussed in blogs.msdn.com/.../chakra-interoperability-means-more-than-just-standards.aspx there is more to achiving interoperability (getting the same code to run in different browsers) than just what is in standards.

  • Anonymous
    September 08, 2010
    The comment has been removed

  • Anonymous
    September 08, 2010
    Steve-- it's a good bug, but did you file it on Connect? Have you tried it in the Platform Preview build?

  • Anonymous
    September 08, 2010
    @Steve,Test: Are you still able to reproduce the problem? I believe that issue was already fixed in an IE8 update (support.microsoft.com/.../976662).

  • Anonymous
    September 08, 2010
    @EricLaw - well spotted... it may well be fixed... there is no info on the JScript blog about it (or update to the post) so I figured it still existed... I'll re-test just to be sure though.

  • Anonymous
    September 08, 2010
    @Mario The main reason why IE is using GPU acceleration is to remove the strain that some have on the CPU.

  • Anonymous
    September 10, 2010
    You spelled "Typo" wrong in your edit - oh the irony...

  • Anonymous
    September 13, 2010
    @Mark LOL!