Deep JavaScript
Please support this book: buy it or donate
(Ad, please don’t block.)

13 Techniques for instantiating classes



In this chapter, we examine several approaches for creating instances of classes: Constructors, factory functions, etc. We do so by solving one concrete problem several times. The focus of this chapter is on classes, which is why alternatives to classes are ignored.

13.1 The problem: initializing a property asynchronously

The following container class is supposed to receive the contents of its property .data asynchronously. This is our first attempt:

class DataContainer {
  #data; // (A)
  constructor() {
    Promise.resolve('downloaded')
      .then(data => this.#data = data); // (B)
  }
  getData() {
    return 'DATA: '+this.#data; // (C)
  }
}

Key issue of this code: Property .data is initially undefined.

const dc = new DataContainer();
assert.equal(dc.getData(), 'DATA: undefined');
setTimeout(() => assert.equal(
  dc.getData(), 'DATA: downloaded'), 0);

In line A, we declare the private field .#data that we use in line B and line C.

The Promise inside the constructor of DataContainer is settled asynchronously, which is why we can only see the final value of .data if we finish the current task and start a new one, via setTimeout(). In other words, the instance of DataContainer is not completely initialized, yet, when we first see it.

13.2 Solution: Promise-based constructor

What if we delay access to the instance of DataContainer until it is fully initialized? We can achieve that by returning a Promise from the constructor. By default, a constructor returns a new instance of the class that it is part of. We can override that if we explicitly return an object:

class DataContainer {
  #data;
  constructor() {
    return Promise.resolve('downloaded')
      .then(data => {
        this.#data = data;
        return this; // (A)
      });
  }
  getData() {
    return 'DATA: '+this.#data;
  }
}
new DataContainer()
  .then(dc => assert.equal( // (B)
    dc.getData(), 'DATA: downloaded'));

Now we have to wait until we can access our instance (line B). It is passed on to us after the data is “downloaded” (line A). There are two possible sources of errors in this code:

In either case, the errors become rejections of the Promise that is returned from the constructor.

Pros and cons:

13.2.1 Using an immediately-invoked asynchronous arrow function

Instead of using the Promise API directly to create the Promise that is returned from the constructor, we can also use an asynchronous arrow function that we invoke immediately:

constructor() {
  return (async () => {
    this.#data = await Promise.resolve('downloaded');
    return this;
  })();
}

13.3 Solution: static factory method

A static factory method of a class C creates instances of C and is an alternative to using new C(). Common names for static factory methods in JavaScript:

In the following example, DataContainer.create() is a static factory method. It returns Promises for instances of DataContainer:

class DataContainer {
  #data;
  static async create() {
    const data = await Promise.resolve('downloaded');
    return new this(data);
  }
  constructor(data) {
    this.#data = data;
  }
  getData() {
    return 'DATA: '+this.#data;
  }
}
DataContainer.create()
  .then(dc => assert.equal(
    dc.getData(), 'DATA: downloaded'));

This time, all asynchronous functionality is contained in .create(), which enables the rest of the class to be completely synchronous and therefore simpler.

Pros and cons:

13.3.1 Improvement: private constructor via secret token

If we want to ensure that instances are always correctly set up, we must ensure that only DataContainer.create() can invoke the constructor of DataContainer. We can achieve that via a secret token:

const secretToken = Symbol('secretToken');
class DataContainer {
  #data;
  static async create() {
    const data = await Promise.resolve('downloaded');
    return new this(secretToken, data);
  }
  constructor(token, data) {
    if (token !== secretToken) {
      throw new Error('Constructor is private');
    }
    this.#data = data;
  }
  getData() {
    return 'DATA: '+this.#data;
  }
}
DataContainer.create()
  .then(dc => assert.equal(
    dc.getData(), 'DATA: downloaded'));

If secretToken and DataContainer reside in the same module and only the latter is exported, then outside parties don’t have access to secretToken and therefore can’t create instances of DataContainer.

Pros and cons:

13.3.2 Improvement: constructor throws, factory method borrows the class prototype

The following variant of our solution disables the constructor of DataContainer and uses a trick to create instances of it another way (line A):

class DataContainer {
  static async create() {
    const data = await Promise.resolve('downloaded');
    return Object.create(this.prototype)._init(data); // (A)
  }
  constructor() {
    throw new Error('Constructor is private');
  }
  _init(data) {
    this._data = data;
    return this;
  }
  getData() {
    return 'DATA: '+this._data;
  }
}
DataContainer.create()
  .then(dc => {
    assert.equal(dc instanceof DataContainer, true); // (B)
    assert.equal(
      dc.getData(), 'DATA: downloaded');
  });

Internally, an instance of DataContainer is any object whose prototype is DataContainer.prototype. That’s why we can create instances via Object.create() (line A) and that’s why instanceof works in line B.

Pros and cons:

13.3.3 Improvement: instances are inactive by default, activated by factory method

Another, more verbose variant is that, by default, instances are switched off via the flag .#active. The initialization method .#init() that switches them on cannot be accessed externally, but Data.container() can invoke it:

class DataContainer {
  #data;
  static async create() {
    const data = await Promise.resolve('downloaded');
    return new this().#init(data);
  }

  #active = false;
  constructor() {
  }
  #init(data) {
    this.#active = true;
    this.#data = data;
    return this;
  }
  getData() {
    this.#check();
    return 'DATA: '+this.#data;
  }
  #check() {
    if (!this.#active) {
      throw new Error('Not created by factory');
    }
  }
}
DataContainer.create()
  .then(dc => assert.equal(
    dc.getData(), 'DATA: downloaded'));

The flag .#active is enforced via the private method .#check() which must be invoked at the beginning of every method.

The major downside of this solution is its verbosity. There is also a risk of forgetting to invoke .#check() in each method.

13.3.4 Variant: separate factory function

For completeness sake, I’ll show another variant: Instead of using a static method as a factory you can also use a separate stand-alone function.

const secretToken = Symbol('secretToken');
class DataContainer {
  #data;
  constructor(token, data) {
    if (token !== secretToken) {
      throw new Error('Constructor is private');
    }
    this.#data = data;
  }
  getData() {
    return 'DATA: '+this.#data;
  }
}

async function createDataContainer() {
  const data = await Promise.resolve('downloaded');
  return new DataContainer(secretToken, data);
}

createDataContainer()
  .then(dc => assert.equal(
    dc.getData(), 'DATA: downloaded'));

Stand-alone functions as factories are occasionally useful, but in this case, I prefer a static method:

13.4 Subclassing a Promise-based constructor (optional)

In general, subclassing is something to use sparingly.

With a separate factory function, it is relatively easy to extend DataContainer.

Alas, extending the class with the Promise-based constructor leads to severe limitations. In the following example, we subclass DataContainer. The subclass SubDataContainer has its own private field .#moreData that it initializes asynchronously by hooking into the Promise returned by the constructor of its superclass.

class DataContainer {
  #data;
  constructor() {
    return Promise.resolve('downloaded')
      .then(data => {
        this.#data = data;
        return this; // (A)
      });
  }
  getData() {
    return 'DATA: '+this.#data;
  }
}

class SubDataContainer extends DataContainer {
  #moreData;
  constructor() {
    super();
    const promise = this;
    return promise
      .then(_this => {
        return Promise.resolve('more')
          .then(moreData => {
            _this.#moreData = moreData;
            return _this;
          });
      });
  }
  getData() {
    return super.getData() + ', ' + this.#moreData;
  }
}

Alas, we can’t instantiate this class:

assert.rejects(
  () => new SubDataContainer(),
  {
    name: 'TypeError',
    message: 'Cannot write private member #moreData ' +
      'to an object whose class did not declare it',
  }
);

Why the failure? A constructor always adds its private fields to its this. However, here, this in the subconstructor is the Promise returned by the superconstructor (and not the instance of SubDataContainer delivered via the Promise).

However, this approach still works if SubDataContainer does not have any private fields.

13.5 Conclusion

For the scenario examined in this chapter, I prefer either a Promise-based constructor or a static factory method plus a private constructor via a secret token.

However, the other techniques presented here can still be useful in other scenarios.

13.6 Further reading