JavaScript’s built-in constructors are difficult to subclass. This chapter explains why and presents solutions.
We use the phrase subclass a built-in and avoid the term extend, because it is taken in JavaScript:
A
B of a given built-in constructor A. B’s instances are also instances of A.
obj
There are two obstacles to subclassing a built-in: instances with internal properties and a constructor that can’t be called as a function.
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):
functionSuper(x,y){this.x=x;// (1)this.y=y;// (1)}functionSub(x,y,z){// Add superproperties to subinstanceSuper.call(this,x,y);// (2)// Add subpropertythis.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:
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
[[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
[[PrimitiveValue]] stores the time represented by a date instance (as the number of milliseconds since 1 January 1970 00:00:00 UTC).
Function
[[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 theRegExpobject.
The only built-in constructors that don’t have internal properties are Error and Object.
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]
functionMyArray(/*arguments*/){vararr=[];// Don’t use Array constructor to set up elements (doesn’t always work)Array.prototype.push.apply(arr,arguments);// (1)copyOwnPropertiesFrom(arr,MyArray.methods);returnarr;}MyArray.methods={getsize(){varsize=0;for(vari=0;i<this.length;i++){if(iinthis)size++;}returnsize;}}
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
2Copying 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
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):
functionSuper(x,y){this.x=x;this.y=y;}functionSub(x,y,z){// Add superproperties to subinstanceSuper.call(this,x,y);// (1)// Add subpropertythis.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).
Inside the subconstructor, create a new superinstance and copy its own properties to the subinstance:
functionMyError(){// Use Error as a functionvarsuperInstance=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{thrownewMyError('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
Delegation is a very clean alternative to subclassing. For example, to create your own array constructor, you keep an array in a property:
functionMyArray(/*arguments*/){this.array=[];Array.prototype.push.apply(this.array,arguments);}Object.defineProperties(MyArray.prototype,{size:{get:function(){varsize=0;for(vari=0;i<this.array.length;i++){if(iinthis.array)size++;}returnsize;}},length:{get:function(){returnthis.array.length;},set:function(value){returnthis.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){returnthis.array[index];}MyArray.prototype.set=function(index,value){returnthis.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(){returnArray.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'