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.
Naively, you would write a constructor Point with frozen instances as follows.
But now you can’t write a sub-constructor [1] ColorPoint:
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 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?
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.
ColorPoint can be implemented in a similar manner:
Obviously, the freezing operation can be extracted into a helper method that all constructors have to call, immediately before they are done:
Then Point and ColorPoint look loke this:
One could even move maybeFreeze to a helper prototype for all freezing constructors (e.g. FrozenObject.prototype):
Obviously, there is no need to change ColorPoint.
We may not want constructors to refer to themselves in a hardcoded manner. That can be avoided via named function expressions [3]:
You can also compare the instance prototype [4] of the current constructor with the instance’s prototype:
Note that checking via the constructor property also relies on the instance prototype. For example, it must have the property correctly set up.
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.
Comments
Post a Comment