A brief tour of Javascript's object model

In this post I will give an overview of Javascript's object model. To a first approximation, by "object model", I mean the mental model that a developer has of an object's structure and behavior. At the end, I will hint at how proxies can be used to implement (variations on) this object model in Javascript itself.

Objects as Maps

Let's start with the simplest possible model most JS developers have of a Javascript object, and then refine as we go along. At its core, a Javascript object is nothing but a flexible bag of properties, which we can think of as a mapping from strings to values:

var point = {
  x: 0,
  y: 0
};

The point object maps the strings "x" and "y" to the value 0.

Javascript has a fairly large set of operations that one can apply to such maps, including the five basic operations applicable to most generic associative containers: property lookup (e.g. point.x), property addition (e.g. point.z = 0), property update (e.g. point.x = 1), property deletion (e.g. delete point.x;) and property query (e.g. "x" in point).

Method Properties

One of the beautiful parts of Javascript, I think, is that support for object "methods" doesn't really require a change to this simple object model: in Javascript, a method is really nothing more than a property whose value happens to be a function. This is reflected in the syntax:

var point = {
  x: 0,
  y: 0,
  toString: function() { return this.x + "," + this.y; }
};

The method invocation point.toString() is essentially a lookup of the "toString" property in the object, followed by a function call that sets this to the receiver object and passes in the arguments: (point.toString).apply(point, []). So, we don't need to adjust our object model to take methods and method invocations into account.

Prototype inheritance

The above object model is not complete: we know that Javascript objects also have a special "prototype" link that points to a parent object, from which the object may inherit additional properties. Objects created using the object literal syntax {…} inherit from the built-in Object.prototype object by default. For instance, our point object inherits from this object the built-in method hasOwnProperty, which allows us to ask whether the object defines a certain property:

point.hasOwnProperty("x") // true

The prototype link of an object can be obtained by calling Object.getPrototypeOf(point), although many browsers also simply represent the prototype link as a regular property of the object with the funny name __proto__.

I don't like to think of the prototype link as a normal property because this link has a large influence on virtually every operation applied to a Javascript object. The prototype link is really special, and setting point.__proto__ to another object has a very large effect on the subsequent behavior of the point object. So, I'd like to think of the prototype link as the next addition to our object model: a javascript object = a map of normal properties + a special prototype link.

Property attributes

The object model just described (objects as maps of strings -> values + a prototype link) is sufficiently accurate to describe user-defined objects in Ecmascript 3. However, Ecmascript 5 extends the Javascript object model with a number of new features, most notably property attributes, non-extensible objects and accessor properties. John Resig has a nice blog post on the subject. I'll only briefly summarize the most salient features here.

Let's start with property attributes. Basically, in ES5, every Javascript property is associated with three attributes, which are simply boolean flags indicating whether the property is:

  • writable: the property can be updated
  • enumerable: the property shows up in for-in loops
  • configurable: the property's attributes can be updated

Property attributes can be queried and updated using the wordy ES5 built-ins Object.getOwnPropertyDescriptor and Object.defineProperty. For example:

Object.getOwnPropertyDescriptor(point, "x")
// returns {
//   value: 0,
//   writable: true,
//   enumerable: true,
//   configurable: true }
Object.defineProperty(point, "x", {
  value: 1,
  writable: false,
  enumerable: false,
  configurable: false
});

This turns "x" into a non-writable, non-configurable property, which is basically like a "final" field in Java. The objects returned by getOwnPropertyDescriptor and passed into defineProperty are collectively called property descriptors, since they describe the properties of objects.

So, in ES5, we need to adjust our object model so that Javascript objects are no longer simple mappings from strings to values but rather from strings to property descriptors.

Non-extensible objects

ES5 adds the ability to make objects non-extensible: after calling Object.preventExtensions(point), any attempt to add non-existent properties to the point object will fail. This is sometimes useful to protect objects used as external interfaces from inadvertent modifications by client objects. Once an object is made non-extensible, it will forever remain non-extensible.

To model extensibility, we need to extend the Javascript object model with a boolean flag that models whether or not the object is extensible. So now, a Javascript object = a mapping from strings to property descriptors + a prototype link + an extensibility flag.

Accessor properties

ES5 standardized the notion of "getters" and "setters", i.e. computed properties. For instance, a point whose "y" coordinate is always equal to its "x" coordinate can be defined as follows:

function makeDiagonalPoint(x) {
  return {
    get x() { return x; },
    set x(v) { x = v; },
    get y() { return x; },
    set y(v) { x = v; }
  };
};

var dpoint = makeDiagonalPoint(0);
dpoint.x // 0
dpoint.y = 1
dpoint.x // 1

ES5 calls properties implemented using getters/setters accessor properties. By contrast, regular properties are called data properties. This point object has two accessor properties "x" and "y". The property descriptor for an accessor property has a slightly different layout than that for a data property:

Object.getOwnPropertyDescriptor(dpoint, "x")
// returns {
//   get: function() { return x; },
//   set: function(v) { x = v; },
//   enumerable: true,
//   configurable: true
// }

Accessor property descriptors don't have "value" and "writable" attributes. Instead, they have "get" and "set" attributes pointing to the respective getter/setter functions that are invoked when the property is accessed/updated. If a getter/setter function is missing, the corresponding "get"/"set" attribute is set to undefined. Accessing/updating an accessor property with an undefined getter/setter fails.

To accommodate accessor properties, we don't need to further extend our object model, other than by noting that a Javascript object maps strings to property descriptors, which can now be either data or accessor property descriptors.

So, in conclusion, a Javascript object =

  • a mapping of strings to data or accessor property descriptors
  • + a prototype link
  • + an extensibility flag

This is an accurate model of user-defined ES5 objects.

Implementing your own objects

The object model described above can be thought of as the interface to a Javascript object. With the introduction of proxies in ES6, Javascript developers actually gain the power to implement this interface, and define their own objects. Not only could you emulate Javascript's default object model in Javascript, you could define small variations to explore different object models.

Examples of such variations include Javascript objects with lazy property initialization, non-extractable or bound-only methods, objects with infinite properties, objects with multiple prototype links supporting multiple-inheritance, etc. There's already some inspiration to be had on github, thanks to David Bruant and Brandon Benvie.

I recently gave a talk at ECOOP that explores these ideas in more detail. The talk builds up by describing Javascript's object model, ties this up to the literature on meta-object protocols, and finally shows how Javascript's Proxy API makes the Javascript meta-object protocol explicit for the first time, allowing developers to implement their own objects. Prototype implementations of proxies are currently available in Firefox and Chrome. To experiment with the latest version of the specified ES6 Proxy API, I recommend using the reflect.js shim.