Subtyping JavaScript built-ins
Join the DZone community and get the full member experience.
Join For FreeJavaScript’s built-ins are difficult to subtype. This post explains why and presents solutions.
Terminology
We use the phrase subtype a built-in and avoid the term extend, because it is taken in JavaScript:- Subtyping a built-in A: Create a sub-constructor B of a given built-in constructor A. B’s instances are also instances of A.
- Extending a built-in A: Adding new methods to A.prototype.
Obstacle 1: instances with internal properties
Most built-in types have instances with so-called internal properties (whose names are written in double square brackets, for example: [[PrimitiveValue]]). Internal properties are managed by the JavaScript engine and usually not directly accessible in JavaScript. The normal subtyping technique in JavaScript is to call a super-constructor as a function with the this of the sub-constructor [2].function Super(x, y) { this.x = x; this.y = y; } function Sub(x, y, z) { // Add super-properties to sub-instance Super.call(this, x, y); // (*) // Add sub-property this.z = z; }Most built-ins ignore the sub-instance passed in as this (*), which is described below, as “obstacle 2”. Additionally, adding internal properties to an existing instance is in general impossible, because they tend to fundamentally change the instance’s nature. Hence, the call at (*) can’t be used to add internal properties. The following types have instances with internal properties:
- Wrapper types: Instances of Boolean, Number and String wrap primitives. They all have the internal property [[PrimitiveValue]] whose value is returned by valueOf(). Additionally, String instances support indexed access of characters.
- Boolean: internal property [[PrimitiveValue]]
- Number: internal property [[PrimitiveValue]]
- String: internal property [[PrimitiveValue]], custom method [[GetOwnProperty]], normal property length. [[GetOwnProperty]] accesses the wrapped primitive string when an array index is used.
- Array: The custom internal 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 or removed and by removing excess elements when length is made smaller.
- Date: internal property [[PrimitiveValue]] stores the time represented by a date instance.
- Function: internal property [[Call]] (the code to execute when the instance is called) and possibly others.
- RegExp: internal property [[Match]] in addition to non-internal properties. Quoting the ECMAScript specification:
The value of the [[Match]] internal property is an implementation dependent representation of the Pattern of the RegExp object.
Work-around. MyArray is a subtype of of Array. It has a getter size that returns the actual elements in an array, ignoring holes (where length counts holes). The trick used to implement MyArray is that it creates an array instance and copies its methods to it. Credit: inspired by a blog post by Ben Nadel [3].
function MyArray(/*arguments*/) { var arr = []; // Don’t use the Array constructor which does not work for, e.g. [5] // (new Array(5) creates an array of length 5 with no elements in it) [].push.apply(arr, arguments); return copyOwnFrom(arr, MyArray.methods); } MyArray.methods = { get size() { var size = 0; for(var i=0; i < this.length; i++) { if (i in this) size++; } return size; } }The above code uses the following helper function:
function copyOwnFrom(target, source) { Object.getOwnPropertyNames(source).forEach(function(propName) { Object.defineProperty(target, propName, Object.getOwnPropertyDescriptor(source, propName)); }); return target; };Interaction:
> var a = new MyArray("a", "b") > a.length = 4; > a.length 4 > a.size 2Caveats. 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 subtypes don’t have instances with internal properties, one still can’t subtype them easily, because the standard pattern for subtyping won’t work (repeated from above):function Super(x, y) { this.x = x; this.y = y; } function Sub(x, y, z) { // Add super-properties to sub-instance Super.call(this, x, y); // (*) // Add sub-property this.z = z; }The problem is that Error always produces a new instance, even if called as a function (*). That is, it ignores the parameter this handed to it via call():
> var e = {} > Object.getOwnPropertyNames(Error.call(e)) [ 'stack', 'arguments', 'type' ] > Object.getOwnPropertyNames(e) []Error does return an instance with own properties, but it’s a new instance, not e. The subtyping pattern only works if Error adds the own properties to this (e, in the above case).
Work-around. Inside the sub-constructor, create a new super-instance and copy its properties to the sub-instance.
function MyError() { // Use Error as a function var superInstance = Error.apply(null, arguments); copyOwnFrom(this, superInstance); } MyError.prototype = Object.create(Error.prototype); MyError.prototype.constructor = MyError;Trying out the new error type:
try { throw new MyError("Something happened"); } catch (e) { console.log("Properties: "+Object.getOwnPropertyNames(e)); }Output on Node.js:
Properties: stack,arguments,message,typeThe instanceof relationship is as it should be:
> new MyError() instanceof Error true > new MyError() instanceof MyError trueCaveat. The main reason for subtyping Error is to have the stack property in sub-instances. Alas, Firefox seems to store that value in an internal property, which is why the above approach does not work there (Firefox 8).
Another solution: delegation
Delegation is a very clean alternative to subtyping. For example, to create your own array type, you keep an array in a property:function MyArray(/*arguments*/) { this.array = []; // Don’t use the Array constructor which does not work for, e.g. [5] // (new Array(5) creates an array of length 5 with no elements in it) [].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 meta-programming.
[ "toString", "push", "pop" ].forEach(function (name) { MyArray.prototype[name] = function () { return Array.prototype[name].apply(this.array, arguments); } });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'
The future: ECMAScript.next
ECMAScript.next will help in two ways. First, it will allow you to assign arbitrary prototypes to instances with internal properties. Second, it will probably allow you to override the [] operator. Then you can simulate arrays without needing to subtype Array.1. Creating special instances with an arbitrary prototype. There will be an operator that allows you to assign an arbitrary prototype to a function or array instance. For example, an array instance arr with all necessary internal properties that has the prototype MyArrayProto:
var arr = MyArrayProto <| [ "a", "b", "c" ];A function instance with a prototype MyFunctionProto is created as follows:
let func = MyFunctionProto <| function () {}One might expect that it would be simpler to automatically make an instance special if a constructor subtypes a built-in such as Array or Function. But given the nature of internal properties, the above solution is probably the most straightforward one.
The <| operator can be used to create a subtype SubArray of Array that works with new and instanceof [source: Allen Wirfs-Brock]:
// Connect constructors and prototypes var SubArray = Array <| function(...values) { // Connect instance and prototype return SubArray.prototype <| [...values]; } // Subtype methods SubArray.prototype.method1 = function() {...}; SubArray.prototype.method2 = function() {...};Interaction:
var s = new SubArray(1,2,3); console.log(s.length); // 3 s[3] = 4; console.log(s.length); // 4 console.log(Object.isArray(s)); // true console.log(s instanceof SubArray); // true console.log(s instanceof Array); // true2. Custom square bracket getters and setters. “Object Model Reformation: Decoupling [ ] and Property Access” by Allen Wirfs-Brock proposes to make the operator [] overridable.
- The best practice is now to use new methods Object.getProperty() and Object.setProperty() whenever you want to use an arbitrary string as a property name.
- Object.prototype uses those methods to implement the operator [] (so that legacy code works as expected).
- New collection classes are free to provide their own implementations for []. One possibility is to use keys that are not strings.
References
- ECMAScript 5.1 specification, Chap. 15 [details on built-ins: instance properties etc.]
- Prototypes as classes – an introduction to JavaScript inheritance
- “Extending JavaScript Arrays While Keeping Native Bracket-Notation Functionality” by Ben Nadel
Opinions expressed by DZone contributors are their own.
Comments