2013-06-08

Freezing instances and the first invoked constructor

Let’s say you want to write a constructor that produces instances that are frozen (immutable). One problem, you have to solve, is: when do you freeze this? If you always – unconditionally – perform the operation in the constructor then you can’t create sub-constructors that have their own instance properties. This blog post explains how to work around this problem.

The problem

Naively, you would write a constructor Point with frozen instances as follows.
    function Point(x, y) {
        this.x = x;
        this.y = y;
        Object.freeze(this);
    }
But now you can’t write a sub-constructor [1] ColorPoint:
    function ColorPoint(x, y, color) {
        Point.call(this, x, y);
        this.color = color;  // Impossible, `this` is frozen
    }
You want super-constructors to initialize their part of an instance before their sub-constructors, so calling super-constructors last is a non-solution.

The solution

The constructor that is currently running can freeze if it is the first constructor that is called (obviously, after it has invoked its immediate super-constructor). But how do we determine that?

Am I the first invoked constructor?

A constructor can find out if it is first in a chain of one or more constructors that are called to create an instance, by checking whether the property constructor [2] of this refers to it. Accordingly, the following implementation of Point can be subtyped.
    function Point(x, y) {
        this.x = x;
        this.y = y;
        if (this.constructor === Point) {
            Object.freeze(this);
        }
    }
    Point.prototype.toString = function () {
        return this.x + ' ' + this.y;
    };
ColorPoint can be implemented in a similar manner:
    function ColorPoint(x, y, color) {
        Point.call(this, x, y);
        this.color = color;
        if (this.constructor === ColorPoint) {
            Object.freeze(this);
        }
    }
    ColorPoint.prototype = Object.create(Point);
    ColorPoint.prototype.constructor = ColorPoint;
    ColorPoint.prototype.toString = function () {
        return Point.prototype.toString.call(this)
               + ' ('+this.color+')';
    };

A helper method for post-processing an instance

Obviously, the freezing operation can be extracted into a helper method that all constructors have to call, immediately before they are done:
    Point.prototype.maybeFreeze = function (constr) {
        if (this.constructor === constr) {
            Object.freeze(this);
        }
    };
Then Point and ColorPoint look loke this:
    function Point(x, y) {
        this.x = x;
        this.y = y;
        this.maybeFreeze(Point);
    }
    function ColorPoint(x, y, color) {
        Point.call(this, x, y);
        this.color = color;
        this.maybeFreeze(ColorPoint);
    }
One could even move maybeFreeze to a helper prototype for all freezing constructors (e.g. FrozenObject.prototype):
    function Point(x, y) {
        ...
    }
    Point.prototype = Object.create(FrozenObject.prototype);
    ...
Obviously, there is no need to change ColorPoint.

Not hardcoding the reference to the constructor

We may not want constructors to refer to themselves in a hardcoded manner. That can be avoided via named function expressions [3]:
    var Point = function me(x, y) {
        this.x = x;
        this.y = y;
        this.afterConstructor(me);
    };

Another way of checking for the first invoked constructor

You can also compare the instance prototype [4] of the current constructor with the instance’s prototype:
    Point.prototype.maybeFreeze = function (constr) {
        if (Object.getPrototypeOf(this) === constr.prototype) {
            Object.freeze(this);
        }
    };
Note that checking via the constructor property also relies on the instance prototype. For example, it must have the property correctly set up.

References

  1. JavaScript inheritance by example
  2. What’s up with the “constructor” property in JavaScript?
  3. Expressions versus statements in JavaScript
  4. JavaScript terminology: the two prototypes

No comments: