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):
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:
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 theRegExp
object.
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]
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
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
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).
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
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'