Constructors in ES6

I’ve mentioned before that I’m not thrilled about classes in ES6. They’re essentially syntactic sugar, and the core concepts of Prototype-based programming (or OLOO) still apply. However, classes are becoming standard, so we need to adapt.

One nice feature in “classic JavaScript” is the ability to return a different object from constructor functions (those invoked with new) instead of the default this object. This is useful for object pools or “implicit/transparent” singleton (a specific type of object pool). I wondered if ES6 class constructors allowed this, and thankfully, they do. this post by Dr Rauschmayer explains this well as “Overriding the result of a constructor.” I hadn’t considered that a class’s prototype points to its parent class, but it makes sense for inheriting static methods. This diagram illustrates how class inheritance translates to the prototypal world:

The entire article is insightful. It highlights that classes aren’t just compile-time sugar. Their introduction has altered the runtime, particularly how the this object is handled in constructors:

  • In ES6, the instance object is created within the base constructor, at the end of the constructor chain.
  • In ES5, it’s created in the new operand, at the beginning of the chain.

ES6 introduces other runtime changes, such as new internal properties for functions: [[ConstructorKind]], [[Construct]], and [[HomeObject]]. Consequently, there are now different function types:

  • Constructible functions: Callable with new (due to [[Construct]]). This includes regular functions and those defined as constructors.
  • Arrow functions: Not constructible, capture the lexical this (similar to Function.bind).
  • Functions from method definitions: Not constructible, use dynamic this, and can use super.

ES6 classes handle missing constructors like other languages. If you don’t define one, the compiler adds a default: constructor() {} for base classes and constructor(...args) {super(...args);} for derived classes.

Since JavaScript functions accept varying parameter counts, function overloading (found in static languages) doesn’t exist. Therefore, a class has one constructor, avoiding the issue in static languages where a base class lacks a parameterless constructor, as described in here.

The article points out an omission in ES6 classes: calling a constructor without new. The emphasis on this surprised me. I’ve never grasped the effort in pre-ES6 libraries (like underscore.js) to allow calling constructor functions with or without new:

1
2
3
4
5
function Person() {
    if (!(this instanceof Person)) return new Person();
    //normal initialization code here 
    //this.property = val;
  };

Protecting against forgetting new seems pointless. Rules exist for a reason. It’s like using “->” instead of “.” for method calls. Rauschmayer explains the use case in the comments:

The use case is if you want to remain flexible w.r.t. implementing something via a class or via a factory function. For my own code, I wouldn’t mind this kind of minor refactoring. For libraries, this becomes more important.

He’s referring to a function receiving an object creation function as a parameter, agnostic to whether it’s a constructor. The function designer aims to support two scenarios: this generic case and the standard constructor call with new:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
function Person() {
    if (!(this instanceof Person)) return new Person();
    //normal initialization code here 
    //this.property = val;
  };

function test(objectCreatorFunc){
   var o1 = objectCreatorFunc();
}

//normal use:
new Person();

//pass it as a factory to the test method
test(Person);

While useful, this is easily circumvented without explicit language support:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
class Person() {
    constructor(){
     //this.property = val;
 }
  };

function test(objectCreatorFunc){
   var o1 = objectCreatorFunc();
}

test(function(){
 return new Person();
});

like this criticizes using new:

The `new` keyword violates both the substitution principle and the open / closed principle. It’s also destructive because it adds zero value to the language, and it couples all callers to the details of object instantiation. If you start with a class that requires `new` (all classes in ES6) and you later decide you need to use a factory instead of a class, you can’t make the change without refactoring all callers. Take a look at this example gist.

Discouraging new in favor of factories seems strange initially, but his point about coupling makes sense. We’ve encountered this concept before with Dependency Injection as an alternative to new. This connects back to my earlier point about constructors returning different objects. Instead of a class managing its instance pool, an external IoC container should handle it.

The entire post is thought-provoking. He argues for deprecating super due to tight coupling. This aligns with his earlier statement about avoiding inheritance (extends) due to the same reason. This reinforces the “favor composition over inheritance” principle. this answer provides a good reminder of why inheritance leads to tight coupling.

http://js-bits.blogspot.fr/2010/08/constructors-without-using-new.html ————- https://medium.com/javascript-scene/the-two-pillars-of-javascript-ee6f3281e7f3#.g3ynqbd4j http://martinfowler.com/bliki/CallSuper.html Dependency Injection

Licensed under CC BY-NC-SA 4.0
Last updated on Apr 22, 2023 20:25 +0100