javascriptconstructorfactoryprototypal-inheritanceprivate-members

Prototype assignment within a factory in order to achieve private field handling versus constructor and private state without prototypal methods


I know there have been a lot of questions about module patterns, prototypical inheritance, ES6 classes, etc with excellent answers. This is not about discussing different patterns or the bad sides of ES6 classes.

My question, to which I couldn't find a clear answer in the existing questions, is about the difference and pros and cons of two specific approaches.

I like the flexibility that comes with the module pattern and I fully understand the pitfalls, limitations and downsides of relying on the prototype chain (whether or not using the class keyword or traditional function with prototype). However, I want (and sometimes need) the objects that I create to be "identifiable" through instanceof.

I'm aware of two approaches that combine module pattern (and ability to do object composition) while tying the object's prototype to the function that created it.

Method 1: Like module pattern, but we manually set the prototype of the object returned

function Bar() {
  let privateVar = 1;
  const someMethod = () => {
    // do something
  };

  return Object.setPrototypeOf({ someMethod }, Bar.prototype);
}
const b = new Bar() // can omit new as well
console.log(b, b instanceof Bar); // gets a nice "Bar" name in the console + instanceof true

Method 2: Wrapper class and assigning all methods in the constructor

class Foo {
  constructor() {
    let privateVar = 1
    this.someMethod = () => {
      // do something
    }
  }
}
const f = new Foo() // must call with new
console.log(f, f instanceof Foo); // same as Method 1

Question

Are there any downsides to either approach, is one clearly better or worse. Or are they both bad in the sense that they are violating the expectation that the class or constructor function can be "extended" and one would expect to find the properties/methods on the prototype?


Solution

  • Are they both bad in the sense that they are violating the expectation that the class or constructor function can be "extended"?

    No. Only your first snippet does not support subclassing, because it does hard-code the prototype to Bar.prototype. The usual way to write this would be to still just use this in the constructor:

    function Bar() {
      let privateVar = 1;
      this.someMethod = () => {
        // do something
      };
    }
    

    If you want to handle callers forgetting the new keyword, there's various approaches to fix this:

    function Bar() {
      if (!(new.target && this instanceof Bar)) throw new Error('Invalid call');
      let privateVar = 1;
      this.someMethod = () => {
        // do something
      };
    }
    function Bar() {
      let privateVar = 1;
      const someMethod = () => {
        // do something
      };
      if (new.target) {
        this.someMethod = someMethod;
      } else {
        return { __proto__: Bar.prototype, someMethod };
      }
    }
    function Bar() {
      const self = new.target ? this : { __proto__: Bar.prototype };
      let privateVar = 1;
      self.someMethod = () => {
        // do something
      };
      return self;
    }
    

    Are they both bad in the sense that one would expect to find the properties/methods on the prototype?

    Yes, but no actually this is no longer generally expected. There is a lot of code out there creating function-valued own properties on instances (using ES5 style, ES6 class syntax or ES2022 class fields), whether that's for creating bound methods or closures over private state or something else.

    It helps if you point this out in your documentation, together with the expectations on contracts that a subclass should fulfill.