Web Development > Property descriptors and the prototype chain

A little while ago a tweet by Kyle Simpson began an interesting discussion about some arguably inconsistent behaviour in JavaScript. The questionable behaviour is setting a property value on an object, in particular the case where that property is already defined by a setter function on the object's prototype.

We'll get to what happens in that particular case a bit later. Irrespective of whether the behaviour is consistent or not, I wanted to clarify to myself how getting and setting property values works and interacts with the prototype chain. It is, of course, all laid out in full detail in the ECMAScript specification, and I would recommend giving it a read if you're interested. But, I still felt the need to put it in my own words, or rather pictures.

I'm going to build up a visual analogy to help illustrate the nuances of getting and setting property values on objects. I'll be representing objects as sheets of glass (don't try this at home folks!) and object properties will be labelled stickers stuck on to that glass. Hopefully the correspondence will become clear as we go along, so here goes...

Properties as they used to be

Let us start with the simple case of properties as we knew them before ECMAScript's fifth edition. For example, suppose we construct a student object, let's say it has a property called college whose value is "Foo". In our analogy, the student object will be represented by a sheet of glass, and the college property will be a sticker, labelled as such, with a value of "Foo".

var student = {
    college: "Foo"
};
 
student.college; // "Foo"

We could change the value of college by erasing it and writing something else on the sticker. We could add new properties to student by adding more stickers to the glass. We can also delete properties by removing stickers.

The next thing we'll do is create a new object, let's call it slacker, whose prototype is the student object. In our analogy the slacker object will be a new sheet of glass, but importantly it is positioned over the top of that of the student object. The reason for using glass now becomes apparent - properties of the underlying object show through and can be read off at the level of the new object.

var student = {
    college: "Foo"
};
 
var slacker = Object.create(student);
 
slacker.college; // "Foo"

Furthermore, if we set properties on the new object we never affect the underlying object - we can't get through the glass!

var student = {
    college: "Foo"
};
 
var slacker = Object.create(student);
slacker.lazy = true;
 
slacker.lazy; // true
 
student.lazy; // undefined

It should then be clear that if we try to set a property that is already defined on the underlying object we merely cover it up, creating a new property on the top level and leaving the underlying object untouched.

var student = {
    college: "Foo"
};
 
var slacker = Object.create(student);
slacker.college = "Bar";
slacker.lazy = true;
 
slacker.college; // "Bar"
slacker.lazy; // true
 
student.college; // still "Foo"
student.lazy; // undefined

Property descriptors

With ECMAScript 5 came a more involved process for getting or setting a property's value. Each object property is now defined in terms of a property descriptor. Let us see how our analogy holds up to this extension.

Property descriptors come in two types, the first of which is data descriptor. This type extends the old way of defining properties - as well as a value you can also specify whether the property is writable. It would be nice to think of non-writable properties as if the value was written in permanent ink, but unfortunately this analogy doesn't quite work, as we'll see shortly. Instead we can think of non-writable properties as a different type of sticker, one with an extra instructional note telling you not to write on it.

var student = {};
 
Object.defineProperty(student, "college", {
    value: "Foo",
    writable: false
});
 
student.college; // "Foo"
 
student.college = "Bar"; // throws in strict mode
student.college; // still "Foo"

Standard value assignment must accept this instructional note. What's more, the same is true in the case of our object we lay down on top, the note shows through to the new object and must be respected. The writable flag is inherited.

var student = {};
 
Object.defineProperty(student, "college", {
    value: "Foo",
    writable: false
});
 
var slacker = Object.create(student);
slacker.lazy = true;
 
slacker.college = "Bar"; // throws in strict mode
slacker.college; // still "Foo"

Note if we had gone with the permanent ink analogy there would be no discernible difference between writable and non-writable properties when we're at the top level. Such an analogy would not allow the writable flag to be inherited.

Accessor descriptors are the second type of property descriptor. These provide get and set functions which define exactly how a property should be retrieved or written. We can think of these as complicated instructions, like little flow charts, that have to be followed. These are really special sticker types, different for each different getter/setter function.

var student = {
    lazy: false
};
 
Object.defineProperty(student, "college", {
    get: function () {
        return this.lazy ? "Bar" : "Foo";
    }
});
 
student.college; // "Foo"

Just like the writable flag, the getters and setters show through to objects layered on top, and must be respected. Getters and setters are inherited.

var student = {
    lazy: false
};
 
Object.defineProperty(student, "college", {
    get: function () {
        return this.lazy ? "Bar" : "Foo";
    }
});
 
var slacker = Object.create(student);
slacker.lazy = true;
 
slacker.college; // "Bar"

In this example, when retrieving the college property at the top level, we see the flowchart showing through from underneath. It appears to be pointing at the lazy value on the top level, so this is what we use as we follow it. Of course, back in the world of JavaScript this merely corresponds to the getter function being called with its this context being the slacker object.

In both these cases - the non-writable data descriptor and the accessor descriptor - the only way we can bypass the behaviour from the underlying object is by explicitly defining a new college property on the slacker object using Object.defineProperty. That will effectively force a sticker to be laid down on top, covering up all behaviour from the underlying sticker. In the case of an underlying property descriptor being a writable data descriptor this happens automatically when we set a value through standard assignment, just as in the pre ES5 case above, however for non-writable or accessor properties we have to be explicit.

Enumerable and configurable

You may like to push the analogy a little further. Property descriptors of either type can also be defined as enumerable and/or configurable.

The enumerable flag is inherited, just as the writable flag and the get/set methods were. So, in our analogy it must be represented by something visible that shows through from the underlying object. I'll represent enumerable properties as stickers that have a green loopy bit!

var student = {};
 
Object.defineProperty(student, "college", {
    value: "Foo",
    enumerable: true
});
 
Object.defineProperty(student, "lazy", {
    value: false,
    enumerable: false
});
 
var slacker = Object.create(student);
 
for (var key in slacker) {
    console.log(key); // logs only "college"
}

Of course you can have enumerable, non-writable properties, these would be stickers with both the no writing part and a green loopy bit. Similarly for enumerable accessor descriptors. I'll leave the visualisation up to you for these cases.

The configurable flag is different, it is not inherited and as such we can choose a more physical representation in our analogy. I like to think of non-configurable properties as stickers that have been stuck down with superglue, they cannot be removed (which would be required for deleting the property or redefining them).

var student = {};
 
Object.defineProperty(student, "college", {
    value: "Foo",
    configurable: false
});
 
// these throw in strict mode
delete student.college; 
Object.defineProperty(student, "college", {
    value: "Bar"
}); 
 
student.college; // still "Foo"

Superglue does not have any effect on the next level, nothing shows through the glass, so properties can be configured on the top level irrespective of whether they're configurable on the underlying level.

var student = {};
 
Object.defineProperty(student, "college", {
    value: "Foo",
    configurable: false
});
 
var slacker = Object.create(student);
 
Object.defineProperty(slacker, "college", {
    value: "Bar"
}); 
 
slacker.college; // "Bar"

Prevent extensions, seal and freeze

There are further actions that can be performed with regard to object properties and their descriptors. The only one that brings anything new to the analogy is Object.preventExtensions. Once it is called on an object it is no longer possible to define new properties on that object. The analogy here would be that all the remaining glass gets coated in Teflon to prevent new stickers being added (and if you do remove a sticker, the Teflon gets spread to the area that sticker was in). Just like the superglue, an underlying layer of Teflon has no effect on the top level, preventing extensions is not inherited.

Object.seal merely makes all properties non-configurable and then prevents extensions on the object. That is, it sticks all the stickers down with superglue and coats the whole thing in Teflon.

Object.freeze does the same the same as Object.seal but beforehand makes all data properties non-writable.

Conclusions

The analogy we've been using here seemed to work well for pre-ES5 style properties. When we push it to include the full spectrum of property descriptors it could be argued that it is a little ad hoc in that some aspects had to be represented visually, such as the writable flag, whereas some were not, such as configurable. Of course this all comes down to the way JavaScript works and which aspects of property descriptors are inherited. It may be that there are better analogies out there for this kind of thing, if so I'd love to hear about them. Hopefully though this gives you a picture to keep hold of in your head, which should make the specifics of property descriptors and their interaction with the prototype chain easier to remember - I think it has for me.