Property Descriptors in JavaScript

Tools IMG_0172 by OZinOH, on FlickrSome of the most common complaints about JavaScript come from its dynamic, mutable nature. You can modify nearly any member of any object and even delete some built-in ones! This enables web developers to improve cross-browser compatibility of their sites while maintaining code readability with a technique called poly-filling. On the other hand, it also allows developers to modify built-in behavior (called “monkey-patching”), sometimes causing side-effects or bugs that are difficult to troubleshoot.

Starting in ECMAScript 5, new mechanisms have been defined to give you (the developer) the ability to restrict what can be done to your object members. You can read the standard here. I did. Twice.

Object.defineProperty

Historically, there has been two primary ways to define a property of an object:

// 1. define object property as part of literal object declaration:
var obj1 = {
  property: 'Property Value'
};
 
// 2. assign object property a value
var obj2 = {};
obj2.property = 'Property Value'; // creates new expando property on the object

Some people find this elegant or even beautiful. Others see this and feel like they need to bathe in hand-sanitizer. Whatever your opinion, there are limitations to this approach. Think of it this way… in ECMAScript 3 and earlier, a property definition consists of name, value and context. That’s it.

Properties are more complex in ECMAScript 5, and there is a new way to define them:

var obj = {};
Object.defineProperty(obj, "property", {value:'Property Value'});

This mechanism is more verbose, but it allows for something that wasn’t possible before. It allows us to control some of the internal attributes of the object properties.

writable

One of the attributes that can be controlled is the writable. This determines whether or not the property value can be changed.

var obj = {};
Object.defineProperty(obj, "constProperty", {value:'Original Value', writable: false});
obj.constProperty = "new value";
alert(obj.constProperty); // "Original Value"

enumerable

The enumerable property attribute is a little more cryptic. It determines whether or not the property is enumerated with all of the other members. This is best explained with an example.

var obj = {};
Object.defineProperty(obj, "showme", {enumerable: true, configurable: true});
Object.defineProperty(obj, "hideme", {enumerable: false, configurable: true});
 
for (var name in obj) alert(name); 
// alerts "showme".... but NOT "hideme"
 
Object.defineProperty(obj, "showme", {enumerable: false});
Object.defineProperty(obj, "hideme", {enumerable: true});
 
for (var name in obj) alert(name); 
// alerts "hideme".... but NOT "showme"

You may have realized by now that “enumerable” is set to false for most of the base object members. That is why built-in functions (like “toString”) do not show up with you iterate over an object’s members.

configurable

The configurable attribute determines whether or not a property descriptor can be changed.

As seen in the last example, property descriptors can be changed in subsequent calls to “defineProperty”. This would normally undermine the intent of “writable: false”.

var obj = {};
Object.defineProperty(obj, "const", {value: "const val", writable: false, configurable: true});
obj.const = "new val";
alert(obj.const); // "const val"
 
Object.defineProperty(obj, "const", {writable: true});
obj.const = "overridden value";
alert(obj.const); // "overridden value"

With the configurable attribute, we can now lock it down.

var obj = {};
Object.defineProperty(obj, "const", {value: "const val", writable: false, configurable: false});
obj.const = "new val";
alert(obj.const); // "const val"
 
try {
  Object.defineProperty(obj, "const", {writable: true}); // CANNOT BE RE-DEFINED!
} catch (e) {
  alert(e);
}
obj.const = "overridden value";
alert(obj.const); // "const val"

As a result, once configurable is set to false, it cannot be undone.

Property Accessors

Genuine property accessors are now available in javascript!

var obj = {};
Object.defineProperty(obj, "property", 
    {
      get:function() { alert('get value'); }, 
      set:function(val) { alert('set value'); }
    });
var temp = obj.property; // alerts "get value"
obj.property = ""; // alerts "set value"

Putting It All Together

Since JavaScript is not OO, you may have surmised that the best place to setup your property definitions is in the object constructor (read more about JS objects and inheritance here). Here is a simple example that brings all of this together.

function Face(shape) {
    Object.defineProperty(this, "Shape", {
        value: shape,
        writable: false,
        enumerable: true,
        configurable: false
    });
    Object.defineProperty(this, "Unibrow", {
        value: false,
        enumerable: false
    });
 
    function isEyeColor(color) {
        return color in ['brown', 'green', 'hazel', 'blue'];
    }
    var eyeColor;
    Object.defineProperty(this, "EyeColor", {
        enumerable: false,
        get: function() {
            return eyeColor;
        },
        set: function(val) {
            if (!isEyeColor(val)) {
                return;
            }
            eyeColor = val;
        }
    });
    Object.defineProperty(this, "PickNose", {
        enumerable: true,
        value: function() {
            alert("Pick and flick.");
        }
    });
}
 
var myFace = new Face("Manly");
myFace.Shape = "Dastardly";
myFace.EyeColor = "brown";
myFace.Unibrow = "Heck no.";
myFace.PickNose(); // alerts "Pick and flick."
for (var name in myFace)
    alert(name + ": " + myFace[name]);// alerts​​​​​​ "Shape" and "PickNose", but not EyeColor​​​​​​​​​​​​​​​​​​​​​​

Bonus points: change EyeColor enumerable to true and see what happens. Then change the for..in loop to JSON.stringify().

A Note About Browser Support and Poly-Fills

Property definitions are supported in current versions of all major browsers, but older browsers are obviously deficient in this respect.

Most of the time, you can use a technique that has come to be called “polyfilling” to fill-in the gaps in functionality. Property descriptors are an area that cannot be polyfilled in older browsers. When I say that, I actually mean cannot be polyfilled. The behavior exhibited with the ability to modify property descriptors can only be implemented in native code.. that is, in the interpreter engine itself.

If you see something that claims to polyfill property descriptors for IE8 (for example), it can be nothing more than a noop polyfill that simply prevents syntax errors.

Just something to keep in mind.

3 Responses to “Property Descriptors in JavaScript”

Leave a Reply