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


IEBlog