In JavaScript, it is difficult to create sub-constructors of built-in constructors such as Array. This blog post explains the problem and possible solutions – including one that will probably be chosen by ECMAScript 6. The post is based on Allen Wirfs-Brock’s slides from a presentation he held on January 29, during a TC39 meeting.
function SuperConstr(arg1) { ... } function SubConstr(arg1, arg2) { SuperConstr.call(this, arg1); } SubConstr.prototype = Object.create(SuperConstr.prototype);If you want to use this pattern with built-in constructors, you are facing problems. The following example demonstrates those problems for Array; they are similar for other builtins.
function MyArray(len) { Array.call(this, len); } MyArray.prototype = Object.create(Array.prototype);This doesn’t work:
> var a = new MyArray(); > a[2] = 'abc'; > a.length 0Compare: a normal array.
> var b = []; > b[2] = 'abc'; > b.length 3If you invoke a constructor C via new C(arg1, arg2, ...), two steps happen (in the internal [[Construct]] method that every function has):
> var a = []; > var b = Array.call(a, 3); > a === b // b is a new object false > b.length 3 > a.length // a is unchanged 0Thus, the Array.call(...) in the first line of MyArray does not work.
function MyArray(len) { var inst = new Array(len); inst.__proto__ = MyArray.prototype; return inst; } MyArray.prototype = Object.create(Array.prototype);Apart from changing the prototype of an existing object being a relatively costly operation, the biggest disadvantage of this solution is that you can’t subclass MyArray in a normal manner, either.
This is the only solution that works in current browsers (that support __proto__).
Function.prototype.[[Construct]] = function (...args) { let Constr = this; // Allocation let inst = Object.create(Constr.prototype); // Initialization let result = Constr.apply(inst, args); if (result !== null && typeof result === 'object') { return result; } else { return inst; } }Array overrides this method to allocate an exotic object.
Eliminating the allocation obstacle. In a subclass of Array, we’d like to reuse method [[Construct]] of Array. In ECMAScript 5, we can’t, because the prototype of a constructor is always Function.prototype and never its super-constructor. That is, it doesn’t inherit [[Construct]] from its super-constructor. However, in ECMAScript 6, constructor inheritance is the default.
Additionally, Wirfs-Brock proposes to handle allocation in a separate, publicly accessible method whose key is the well-known symbol @@create (that can be imported from some module). Array would only override that method and default [[Construct]] would look like this for all constructors:
Function.prototype.[[Construct]] = function (...args) { let Constr = this; // Allocation let create = Constr[@@create] || Object[@@create]; let inst = create(); // Initialization let result = Constr.apply(inst, args); if (result !== null && typeof result === 'object') { return result; } else { return inst; } }Many builtins would have custom @@create methods: Array, String, Boolean, Number, Date, RegExp, Map, Set, Weakmap, ArrayBuffer, ... These builtins have exotic instances and/or custom values for the internal property [[Class]] [3].
Eliminating the initialization obstacle. Sub-constructors need to be able to use Array for initialization. Thus, it needs to distinguish two different kinds of invocation:
function Foo() { 'use strict'; if (this === undefined) { // (*) // Invoked as a function } else { // Invoked for initialization } }However, this fails if you put Foo into a namespace object:
var namespace = {}; namespace.Foo = Foo; namespace.Foo(); // not treated as a function callIn line (*), you need to ensure that this is not an instance of Foo:
if (this === undefined || !(this instanceof Foo)) ...You could also trigger the “function” case for instances of Foo that have already been initialized.
This solution will probably be adopted by ECMAScript 6. Its complexity will be largely hidden: You can either use the canonical way of subclassing shown above or you can use a class definition [4]:
class MyArray extends Array { ... }
Solution. You need to distinguish whether you are called directly via new or via a sub-constructor. It is conceivable to add language support for this. An alternative is to have a parameter calledFromSubConstructor whose default value is false. Sub-constructor set it to true. If it is true, you initialize. Otherwise, you return the result of a sub-constructor.
> var obj = {}; > new Object(obj) === obj trueAgain, this can be solved in the manner described in Sect. 3.1.
let result = Constr.apply(inst, args);It is equivalent to:
let result = inst.constructor(...args);Furthermore, sub-constructors call super-constructors to help them with initialization. For example:
function SubConstr(arg1, arg2) { super.constructor(arg1); // or: super(arg1) }Lastly, even though a class is internally translated to a constructor, the actual constructor body is inside a method:
class MyClass { constructor(...) { ... } ... }
let inst = Object.create(MyConstr.proto); inst.constructor(arg1, arg2);Instead, you have to do the following.
let inst = MyConstr[@@create](); inst.constructor(arg1, arg2);