Saturday, January 31, 2015

ES 5 Properties and the Browser Object Model

In ES 5 a very powerful concept was added to the JavaScript language. The concept was properties. A property is a getter/setter pair of functions that is executed depending on the syntax used when working with the property. This is very similar to other languages such as C# and some C++ CX extensions. Actually it is very similar to almost every other language since properties are kind of a mainstream thing.

Behind the scenes, even in ES 3, the language itself had the capability built in (in the form of something we called built-ins ;-). You probably saw this when you manipulated the length property of an Array instance. You could see a side effect by setting the property versus simply retrieving it. Behind the scenes two different pieces of code were running. Also, in the browser, all of the DOM objects behaved the same, as if they had separate code running when you performed a get versus a set.

But the vanilla objects you wrote yourself didn't have this cool functionality. In fact, it was non-trivial to determine when your objects changed. For this reason, tons of patterns were introduced to simply have two named functions such as getFoo and setFoo along with a backing field to store the actual value. If the value was computed then the backing field wasn't necessary. This still led to unnatural syntax though as the familiar property syntax no longer worked and instead you had to perform a function call. But still, only for vanilla objects, not for the script engine OR the browser...

Fields versus Properties

In ES 5 your vanilla object was configured as a set of fields. Each name represents a storage location on the object that can store any other JavaScript object. That might be a number, string, function or other object. The platform and browser on the other hand only had properties with no concept of a field. Fields, it turns out, had much better performance (memory accesses) versus properties (find and execute code to do memory accesses). So something really needed to change. Since really we needed fields and properties and we needed both concepts everywhere.

Enter ES 5 and IE 9. When implementing ES 5 in the browser, we worked closely with the Chakra team who was upgrading their object model as well. Instead of built-ins, we instead stored our type system in property descriptors, another ES 5 concept and could now create fields when performance was critical and there were no side effects when setting the value. We could also wrap our getter and setter in a pair of JavaScript function objects.

A side benefit of the property descriptor approach is that we also enabled features like configurability (changing the getters/setters for a property or swapping it to a field), sealing (changing the extensibility and locking objects down), and most importantly, wrapping. In actuality the most IMPORTANT reason for web developers and the primary reason we did all of this, was to enable proper wrapping, which is a critical component for proper extensibility.

Wrapping and Extensibility

So what is this wrapping thing and why does it matter? It matters because the browser has a default behavior for a property that already exists. Take something like the <img> tag which as a src property. It also has an attribute accessible through setAttribute, but for now lets pretend that doesn't exist. The only way to change the source of an image is to use the src property.

So now imagine that we want to sniff and provide some new functionality for the src property? What if we wanted to find some way to simulate the data protocol or some new protocol by rewriting the underlying src to something else? Well, to do that we can just configure the property. We can configure the property for all images or just for the instance. The following demonstrates how to do this for IE. FireFox should work here, but Chrome will not since they define "src" to be a field descriptor on the instance of the image. Ouch, that's two major infractions making it hard for developers to provide their own customized functionality.

Notice how we just BROKE images ;-) Wouldn't it be nicer if we could just get the browser's default implementation and use that instead? Basically just do a pass through from our function to the original browser function. Also, if someone else comes along, since we've added our getter/setter functions to the object or prototype the wrapping can work many levels deep allowing true composition of the object model. Here is the last example for wrapping that puts everything together.

More Type System Benefits

There are many more benefits to this type system construction. For instance, in IE today, as of IE 9 mode, you can inspect our entire type system. We don't hide anything. By walking the prototype objects and constructors on the window object you can discover every method and property that we support. You can determine if they are read-only or writable. And there is basically nothing that is defined on the instances, meaning you can easily rewrite the entire type system and create the object model that you want for your site.

To see how powerful this is we can write some code that walks the entire IE DOM and determines how many browser specific features we implement. A similar script is how I created the data for this Tweet showing our reduction in ms properties and our increase in webkit properties.

I was hoping to do that for Chrome as well and show the interoperability convergence of our two browsers, but unfortunately putting everything on instances defeats this type of simple inspection.

ES 6 Futures

As we move towards ES 6, web developers are going to discover properties and their useful characteristics because we are making them easier and easier to use. New syntax in the language removes the structured data syntax that you have to write today in combination with defineProperty/defineProperties. TypeScript has offered some of this for a while, but native support in the browser without intermediate compilers is really required for broader adoption.

It will become increasingly more important for the browser defined objects to behave precisely like the native script objects. Divergences like what we have in Chrome where everything is a field on the instance, no matter what the reasoning might be, will simply be unacceptable. Both FireFox and IE have set the bar here and produced a highly extensible object model and proven it could be done.