JavaScript’s constructors have never been particularly popular: Douglas Crockford
doesn’t like themand recently, more anti-constructor material has been published (two examples: blog posts by
Kyle Simpsonand
Eric Elliott).
In this blog post, I explain that not all of the constructors’ flaws are real. But even with those that are real, I still recommend to use them. I’ll tell you why and what the future holds for constructors.
function Foo(arg) { if (!(this instanceof Foo)) { return new Foo(arg); } this.arg = arg; }The main reason against this pattern is consistency: you should either always use new or never. A mix makes your code less readable. Thankfully, there are other ways of protecting yourself against forgetting new (described below). Those ways have the added benefits of being simpler than the above pattern.
> new Array(3).length 3 > Array(3).length 3Similarly, the function Object converts non-objects to objects and returns objects unmodified. This constructor behaves the same with new (which unfortunately runs counter to the semantics of new):
> var obj = {}; > Object(obj) === obj true > new Object(obj) === obj trueHowever, there are also several built-ins where new produces different results from a function call:
> typeof String('abc') 'string' > typeof new String('abc') 'object' > typeof Date() 'string' > typeof new Date() 'object'
new Date(...[2011, 11, 24]) // Christmas Eve 2011In ECMAScript 5, you can use apply() for spreading, but it only works with functions, not with constructors. This is indeed an argument for making a constructor work without new. However, it is relatively easy to implement a tool function that spreads while invoking a constructor via new (for details, see [1]):
> Date.construct([2011, 11, 24]) Sat Dec 24 2011 00:00:00 GMT+0100 (CET)
function Color(name) { this.name = name; }If we now create an instance of Color and omit new, things fail silently, without a warning: the result is undefined which will lead to problems later on.
> var c = Color('green'); > c undefinedAdditionally, we have just accidentally created the global variable name, because in non-strict mode, this points to the global object (window in browsers) during a function call.
> name 'green'The strict mode version of Color looks like this:
function Color2(name) { 'use strict'; this.name = name; }In strict mode, this is undefined during function calls. Which is why we now get a warning:
> var c2 = Color2('green'); TypeError: Cannot set property 'name' of undefinedYou may be tempted to throw an exception if a constructor is called as a function:
function Color2(name) { 'use strict'; if (this === undefined) { throw new Error('Constructor called as a function'); } this.name = name; }However, this still does not protect you from forgetting new if you use a namespace (because this will refer to the namespace when you do so):
> var namespace = {}; > namespace.Color2 = Color2; > namespace.Color2('green') // no exception! undefined > namespace.name 'green'
class Expression { public static Expression parse(String str) { if (...) { return new Addition(..); } else if (...) { return new Multiplication(...); } else { throw new ExpressionException(...); } } } ... Expression expr = Expression.parse(someStr);Dart has factory constructors for this purpose. In JavaScript, you can simply return whatever object you need from a constructor [3]. Thus, the JavaScript version of the above code would look as follows.
function Expression(str) { if (...) { return new Addition(..); } else if (...) { return new Multiplication(...); } else { throw new ExpressionException(...); } } ... var expr = new Expression(someStr);This is good news: JavaScript constructors don’t lock you in, you can always change your mind as to whether a constructor should return a direct instance or something else.
Alas, we’re stuck with constructors. So far, the JavaScript community has not agreed on a common inheritance library (which would help tooling and code portability) and it is doubtful that that will ever happen. That means, we’re stuck with constructors under ECMAScript 5.
The most pressing constructor pain point is subclassing. Node.js has the utility function util.inherits() that only tackles subclassing, nothing else (e.g., it does not help with defining constructors). I’d love this function to be ported to browsers (preferably via a UMD module) and gain widespread use everywhere.
ES6 classes internally desugar to constructors. This is not optimal, because the sugared version looks quite different from the desugared version. That is bound to surprise people in the future when they are trying to find out how classes actually work. In contrast, prototypal inheritance is a much better match for the structure of classes. Hence, desugaring them to something prototypal would have been cleaner.
On the other hand, backward compatibility is a strong reason in favor of constructors. And one of the main goals for classes was to make them as lightweight as possible. Therefore, even though I’m not completely happy with classes, I think they are the best currently possible solution and an important part of ES6 that deserves everyone’s support.
class Sub extends mixin(Super, Mixin1, Mixin2) { ... }An instance subInstance of Sub would have the following prototype chain.
Super.prototype ↑ Mixin1, Mixin2 (merged) ↑ Sub.prototype ↑ subInstance
Ideals may tell us something important about what we would like to be. But compromises tell us who we are.Under ECMAScript 5, we have many inheritance APIs, leading to reduced portability of code. If your framework doesn’t force you to use an inheritance API, it is best to use vanilla constructor functions, possibly along with a utility for handling subclassing.
— Avishai Margalit
Under ECMAScript 6, using classes [4] is the obvious choice. They help with subclassing, let you subclass built-in constructors [5] and more.
Both solutions are compromises, but especially classes will make JavaScript a friendlier language and unify a currently very divided inheritance landscape. Many custom inheritance APIs have been created to help with data binding. ECMAScript 7 will remove this last reason for custom APIs via built-in support for data binding, via Object.observe().