1

I am trying to build document indexes in javascript, and am having trouble figuring out the correct way to do work in a es6 constructor.

  • If I dont call buildIndex, the object is not usable, so it seems like a good candidate for the constructor
  • If I call super first it builds the index without the filter - so its not the correct index, or I need to throw away the old index
  • If I set the this.filter first it throws an error that I haven't called super yet.

The only solution I can figure is make users call buildIndex explicitly after construction - which seems counter intuitive and incorrect as if I need to call "construct" after construction.

Am I missing something or are ES6 constructors limited?


class TokenIndex {
    constructor(document, stemmer) {
        this.document = document;
        this.stemmer = stemmer || (x => x);
        this._buildIndex();
    }
    _buildIndex(){
        // do expensive index build
    }
}
class FilteredTokenIndex extends TokenIndex {
    constructor(document, stemmer, filter) {
        this.filterRegex = filter;
        // Fails because super must be called before `this`
        super(document, stemmer); 
    }
    _buildIndex(){
        // do expensive index build
    }    
}

class FilteredTokenIndex2 extends TokenIndex {
    constructor(document, stemmer, filter) {
        // Fails because builds the index without a filter
        super(document, stemmer); 
        this.filterRegex = filter;

    }
    _buildIndex(){
        // do expensive index build
    }    
}
bobbysmith007
  • 326
  • 2
  • 9
  • 1
    Can you not pass filterRegex as part of the ctor for TokenIndex? – Phil Cooper Mar 13 '18 at 16:52
  • 1
    Unfortunately you'll have to implement the subclass using plain ES5 `function`s if the subclass needs to do work before the superclass behavior can be invoked. Instead of `super(document, stemmer)` you can call `TokenIndex.call(this, document, stemmer)` – Patrick Roberts Mar 13 '18 at 16:52
  • What exactly does your `buildIndex` function do, manipulate the `document`? – Bergi Mar 13 '18 at 17:20
  • buildIndex in this is a contrived example, in my real code, it downcases, tokenizes and stems the document using different stemmers, then either builds hashes, trees and or concatenated strings from the stemmed document. As all that can be one of the more time consuming tasks, doing it, throwing away the result and rebuilding the index seemed no good. – bobbysmith007 Mar 13 '18 at 17:25

3 Answers3

3

An ES6-based solution is to not put anything in the base constructor that requires the derived-class to be fully initialized. Instead, put that logic in a .init() method.

Then, create a factory function that does both the new and the .init() and then returns a fully formed object.

class TokenIndex {
    constructor(document, stemmer) {
        this.document = document;
        this.stemmer = stemmer || (x => x);
    }
    init() {
        this._buildIndex();
        return this;
    }
    _buildIndex(){
        // do expensive index build
    }
}
class FilteredTokenIndex extends TokenIndex {
    constructor(document, stemmer, filter) {
        super(document, stemmer); 
        this.filterRegex = filter;
    }
    _buildIndex(){
        // do expensive index build
    }    
}

// Factory functions that should be exported and made public
// and should be the only way these instances can be created
// by the outside world
createTokenIndex(document, stemmer) {
    let obj = new TokenIndex(document, stemmer);
    return obj.init();
}

createFilteredTokenIndex(document, stemmer, filter) {
    let obj = new FilteredTokenIndex(document, stemmer, filter);
    return obj.init();
}

These factory functions could also be made static methods of the class, but I prefer to not export the class at all because that keeps outside users from instantiating it themselves with new and potentially messing up the initialization of the object.


FYI, a similar design pattern can be used when you need to do asynchronous operations in the initialization of an object. In that case, the .init() method returns a promise that resolves to the object itself when all the async operations are done. The factory function then returns that promise. The advantage of using the factory function in both of these cases is that the outside world never gets to use the object until it's fully initialized.

jfriend00
  • 683,504
  • 96
  • 985
  • 979
  • Depending on what those `init` methods do, it might be simpler to put the code right inside those `create` functions. – Bergi Mar 13 '18 at 17:31
  • @Bergi - Yes, that's an option. I assumed they are operating on the object's data so they would naturally be methods, but the code could go either place. – jfriend00 Mar 13 '18 at 17:32
  • What is the proposed advantage of this over calling `init` directly in the constructor (ie using new.target to ensure init is only called in the instantiated class)? It seems like if the constructor never returns a fully constructed object, that it is not much of a constructor. Stated differently: why are static factory functions preferable to working class constructors, and if they are, why are constructors provided at all? – bobbysmith007 Mar 13 '18 at 17:36
  • @bobbysmith007 - You created a situation where your base constructor wants the derived object to be fully instantiated. ES6 constructors just don't work that way. This is a work around for that. You can't call `.init()` in the base constructor because the derived object is not yet initialized. There is no such thing as a purely ES6 constructor solution here that has the base object constructor calling a method that requires the derived object to be fully initialized. ES6 constructors just don't do that. As Bergi has pointed out, this limitation of the language is not unique to ES6 Javascript. – jfriend00 Mar 13 '18 at 19:07
  • @bobbysmith007 - Factory functions are a perfectly suitable alternative when you want to encapsulate a series of initialization operations that, for whatever reason, can't all be done in the constructor. The point of my last paragraph was to show you that there are other reasons for the factory function design pattern too. It's a perfectly acceptable way of doing things when a normal constructor is too limited. – jfriend00 Mar 13 '18 at 19:09
  • @bobbysmith007 - You could call `.init()` in every derived constructor (and not call it in the base constructor) as long as that doesn't create problems inheriting from that class too. But, you seemed to want to initialize things in the base object because it appears the base object can be instantiated on its own so it has to initialize itself. – jfriend00 Mar 13 '18 at 19:11
  • Thanks for your comments and answer. I agree that factories are acceptable when things become asynchronous or become "too much" for the common constructor. This seemed to be a very straightforward case, that I have successfully used in Python, Common Lisp and non-class-based javascript many times, and not being able to duplicate that in ES6 confused me. (Generally I would call super after setting the "filter" slot.) As my normal patterns were failing, it seemed I must have missed something along the way. I still think I prefer the `new.target` answer, but appreciate the education. – bobbysmith007 Mar 13 '18 at 19:26
2

Don't do any work in a constructor (especially when the work is asynchronous). A constructor should just initialise the instance, nothing else.

If the instance is unusable without the work done, you can do it before the construction, in a static method:

class TokenIndex {
    constructor(index, document, stemmer) {
        this.index = index;
        this.document = document;
        this.stemmer = stemmer;
    }
    static buildFrom(document, stemmer = (x => x)) {
        // do expensive index build
        return new this(/* result of work */, document, stemmer);
    }
}
class FilteredTokenIndex extends TokenIndex {
    buildFrom(document, filter, stemmer) {
        // do expensive index build
        return new this(/* result of work */, document, stemmer);
        // or if the filtering is just some preprocessing for the index building,
        return super.buildFrom(filteredDocument, stemmer);
    }
}

Am I missing something or are ES6 constructors limited?

No, you're not missing anything. A constructor must not call overridable methods in about any programming language out there.

Bergi
  • 630,263
  • 148
  • 957
  • 1,375
  • A static factory method was definitely going to be what happened if I couldn't make it work. That said, the index is useless if we dont actually build the index. Not building the index would leave a `var i = new Index(); i.build(); ` which seemed wrong too. Why should a contructor *not* actually contruct the object? Why is the other answer less preferable? – bobbysmith007 Mar 13 '18 at 17:27
  • "A constructor must not call overridable methods in about any programming language out there." It works in both common lisp, javascript and python, and I was *fairly* sure in C# (though its been a long time since I work in a bondage-and-dicipline language. So definitely not "any programming lanugage out there" – bobbysmith007 Mar 13 '18 at 17:31
  • @bobbysmith007 Well no, it might be allowed but *doesn't* work, they all experience the same problem that you are dealing with here in JavaScript. See [here for C#](https://stackoverflow.com/q/119506/1048572) or [here for C++](https://stackoverflow.com/q/962132/1048572) – Bergi Mar 13 '18 at 17:38
  • There are method ordering issues in all inheritance systems, especially multiple inheritance. This issue you speak of in this case is solved by simply preinitializing, then calling the super constructor (which i discovered is invalid in es6 classes). The other solution is to verify that you are the primary constructor, before doing the rest of the work, allowing only the class being initialized to execute the work. Either way they work in many cases, though perhaps not all. – bobbysmith007 Mar 13 '18 at 18:11
  • @bobbysmith007 I think very few languages allow pre-initialising (which also opens another can of worms). ES6 is not one of them. – Bergi Mar 13 '18 at 18:19
  • Thanks very much for your input! Having lived in dynamic languages for so long, some of the finer points of statically typed, class based systems, have evaded my notice. This whole thing has been eye opening – bobbysmith007 Mar 13 '18 at 18:26
0

The answer seems to be making use of the new.target, to identify if this is the actual class being constructed, or a parent and only executing the real work when they match. Finally found the answer in this mozilla article:

https://hacks.mozilla.org/2015/08/es6-in-depth-subclassing/

class TokenIndex {
  constructor(document, stemmer) {
    this.document = document;
    this.stemmer = stemmer || (x => x);
    if (new.target == TokenIndex){
      this._buildIndex();
    }
  }
  _buildIndex(){
    console.log('Build TokenIndex') // do expensive index build
  }
}
class FilteredTokenIndex extends TokenIndex {
  constructor(document, stemmer, filter) {
    super(document, stemmer); 
    this.filterRegex = filter;
    if (new.target == FilteredTokenIndex){
      this._buildIndex();
    }
  }
  _buildIndex(){
    // do expensive index build
    console.log('Build FilteredTokenIndex')
  }    
}


var ti = new TokenIndex(); // prints Build TokenIndex
var fti = new FilteredTokenIndex(); // prints Build FilteredTokenIndex

Thanks

bobbysmith007
  • 326
  • 2
  • 9
  • 1
    You can also use `this.constructor === TokenIndex` since the ES6 `class` constructor already enforces the requirement of calling the function as a constructor (with `new`), so it's identical to `new.target === TokenIndex`. Anyway, good self-answered question. – Patrick Roberts Mar 13 '18 at 17:03