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 constructorA
.B
’s instances are also instances ofA
. -
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
, andString
wrap primitives. They all have the internal property[[PrimitiveValue]]
whose value is returned byvalueOf()
;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 propertylength
.[[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 thelength
property works correctly, by keepinglength
up-to-date when array elements are added and by removing excess elements whenlength
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 theRegExp
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'
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.