Chapter 28. Subclassing Built-ins

JavaScript’s built-in constructors are difficult to subclass. This chapter explains why and presents solutions.

Terminology

We use the phrase subclass a built-in and avoid the term extend, because it is taken in JavaScript:

Subclassing a built-in A
Creating a subconstructor B of a given built-in constructor A. B’s instances are also instances of A.
Extending an object obj
Copying one object’s properties to another one. Underscore.js uses this term, continuing a tradition established by the Prototype framework.

There are two obstacles to subclassing a built-in: instances with internal properties and a constructor that can’t be called as a function.

Obstacle 1: Instances with Internal Properties

Most built-in constructors have instances with so-called internal properties (see Kinds of Properties), whose names are written in double square brackets, like this: [[PrimitiveValue]]. Internal properties are managed by the JavaScript engine and usually not directly accessible in JavaScript. The normal subclassing technique in JavaScript is to call a superconstructor as a function with the this of the subconstructor (see Layer 4: Inheritance Between Constructors):

function Super(x, y) {
    this.x = x;  // (1)
    this.y = y;  // (1)
}
function Sub(x, y, z) {
    // Add superproperties to subinstance
    Super.call(this, x, y);  // (2)
    // Add subproperty
    this.z = z;
}

Most built-ins ignore the subinstance passed in as this (2), an obstacle that is described in the next section. Furthermore, adding internal properties to an existing instance (1) is in general impossible, because they tend to fundamentally change the instance’s nature. Hence, the call at (2) can’t be used to add internal properties. The following constructors have instances with internal properties:

Wrapper constructors

Instances of Boolean, Number, and String wrap primitives. They all have the internal property [[PrimitiveValue]] whose value is returned by valueOf(); String has two additional instance properties:

  • Boolean: Internal instance property [[PrimitiveValue]].
  • Number: Internal instance property [[PrimitiveValue]].
  • String: Internal instance property [[PrimitiveValue]], custom internal instance method [[GetOwnProperty]], normal instance property length. [[GetOwnProperty]] enables indexed access of characters by reading from the wrapped string when an array index is used.
Array
The custom internal instance method [[DefineOwnProperty]] intercepts properties being set. It ensures that the length property works correctly, by keeping length up-to-date when array elements are added and by removing excess elements when length is made smaller.
Date
The internal instance property [[PrimitiveValue]] stores the time represented by a date instance (as the number of milliseconds since 1 January 1970 00:00:00 UTC).
Function
The internal instance property [[Call]] (the code to execute when an instance is called) and possibly others.
RegExp

The internal instance property [[Match]], plus two noninternal instance properties. From the ECMAScript specification:

The value of the [[Match]] internal property is an implementation dependent representation of the Pattern of the RegExp object.

The only built-in constructors that don’t have internal properties are Error and Object.

Workaround for Obstacle 1

MyArray is a subclass of of Array. It has a getter size that returns the actual elements in an array, ignoring holes (where length considers holes). The trick used to implement MyArray is that it creates an array instance and copies its methods into it:[22]

function MyArray(/*arguments*/) {
    var arr = [];
    // Don’t use Array constructor to set up elements (doesn’t always work)
    Array.prototype.push.apply(arr, arguments);  // (1)
    copyOwnPropertiesFrom(arr, MyArray.methods);
    return arr;
}
MyArray.methods = {
    get size() {
        var size = 0;
        for (var i=0; i < this.length; i++) {
            if (i in this) size++;
        }
        return size;
    }
}

This code uses the helper function copyOwnPropertiesFrom(), which is shown and explained in Copying an Object.

We do not call the Array constructor in line (1), because of a quirk: if it is called with a single parameter that is a number, the number does not become an element, but determines the length of an empty array (see Initializing an array with elements (avoid!)).

Here is the interaction:

> var a = new MyArray('a', 'b')
> a.length = 4;
> a.length
4
> a.size
2

Caveats

Copying methods to an instance leads to redundancies that could be avoided with a prototype (if we had the option to use one). Additionally, MyArray creates objects that are not its instances:

> a instanceof MyArray
false
> a instanceof Array
true

Obstacle 2: A Constructor That Can’t Be Called as a Function

Even though Error and subclasses don’t have instances with internal properties, you still can’t subclass them easily, because the standard pattern for subclassing won’t work (repeated from earlier):

function Super(x, y) {
    this.x = x;
    this.y = y;
}
function Sub(x, y, z) {
    // Add superproperties to subinstance
    Super.call(this, x, y);  // (1)
    // Add subproperty
    this.z = z;
}

The problem is that Error always produces a new instance, even if called as a function (1); that is, it ignores the parameter this handed to it via call():

> var e = {};
> Object.getOwnPropertyNames(Error.call(e)) // new instance
[ 'stack', 'arguments', 'type' ]
> Object.getOwnPropertyNames(e) // unchanged
[]

In the preceding interaction, Error returns an instance with own properties, but it’s a new instance, not e. The subclassing pattern would only work if Error added the own properties to this (e, in the preceding case).

Workaround for Obstacle 2

Inside the subconstructor, create a new superinstance and copy its own properties to the subinstance:

function MyError() {
    // Use Error as a function
    var superInstance = Error.apply(null, arguments);
    copyOwnPropertiesFrom(this, superInstance);
}
MyError.prototype = Object.create(Error.prototype);
MyError.prototype.constructor = MyError;

The helper function copyOwnPropertiesFrom() is shown in Copying an Object. Trying out MyError:

try {
    throw new MyError('Something happened');
} catch (e) {
    console.log('Properties: '+Object.getOwnPropertyNames(e));
}

here is the output on Node.js:

Properties: stack,arguments,message,type

The instanceof relationship is as it should be:

> new MyError() instanceof Error
true
> new MyError() instanceof MyError
true

Another Solution: Delegation

Delegation is a very clean alternative to subclassing. For example, to create your own array constructor, you keep an array in a property:

function MyArray(/*arguments*/) {
    this.array = [];
    Array.prototype.push.apply(this.array, arguments);
}
Object.defineProperties(MyArray.prototype, {
    size: {
        get: function () {
            var size = 0;
            for (var i=0; i < this.array.length; i++) {
                if (i in this.array) size++;
            }
            return size;
        }
    },
    length: {
        get: function () {
            return this.array.length;
        },
        set: function (value) {
            return this.array.length = value;
        }
    }
});

The obvious limitation is that you can’t access elements of MyArray via square brackets; you must use methods to do so:

MyArray.prototype.get = function (index) {
    return this.array[index];
}
MyArray.prototype.set = function (index, value) {
    return this.array[index] = value;
}

Normal methods of Array.prototype can be transferred via the following bit of metaprogramming:

[ 'toString', 'push', 'pop' ].forEach(function (key) {
    MyArray.prototype[key] = function () {
        return Array.prototype[key].apply(this.array, arguments);
    }
});

We derive MyArray methods from Array methods by invoking them on the array this.array that is stored in instances of MyArray.

Using MyArray:

> var a = new MyArray('a', 'b');
> a.length = 4;
> a.push('c')
5
> a.length
5
> a.size
3
> a.set(0, 'x');
> a.toString()
'x,b,,,c'



[22] Inspired by a blog post by Ben Nadel.

Get Speaking JavaScript now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.