0

JavaScript has an OO quirk in that method calls in a superclass method will invoke subclass methods. I find I can work around this fairly easily except for constructors. The problem is that when constructing a subclass, the object is not available until super() is called. Any methods called by the superclass constructor that are overriden in the subclass will find an object that has not been initialized by the subclass. Here's an example:

class Employee {
  constructor (name, group) {
    this.name = name;
    this.setGroup(group);
  }
  setGroup (group) {
    this.group = group;
  }
}

class Manager extends Employee {
  constructor (name, group, subordinates) {
    super(name, group);
    this.subordinates = subordinates.map(name => new Employee(name, group));
  }
  setGroup (group) {
    super.setGroup(group);
    this.subordinates.forEach(sub => sub.setGroup(group));
  }
}
const mgr = new Manager('Fred', 'R&D', ['Wilma', 'Barney']);

This will fail in Employee.setGroup because this.subordinates has not been initialized.

One solution is to only call internal methods in the superclass constructor (e.g. _setGroup()), and provide public wrappers that can be overriden in the child. However, this is tedious as any methods called by the constructor can call other methods as well.

I came up with an alternative:


/**
 * Call a function that executes methods from this class, bypassing any
 * method in a subclass.
 * @param {Function} ctor - A class or Function constructor
 * @param {Object} self - An instance of the class
 * @param {Function} fn - A function to call. "this" will be set to self. Any method
 *  calls on self will ignore overriding methods in any subclass and use the
 *  ctor's methods.
 */
/* exported useClassMethods */
function useClassMethods (ctor, self, fn) {
    const subProto = Object.getPrototypeOf(self);
    // temporarily set the object prototype to this (super)class
    Object.setPrototypeOf(self, ctor.prototype);
    try {
        fn.call(self);
    } catch (error) {
        throw(error);
    } finally {
        // make sure the prototype is reset to the original value
        Object.setPrototypeOf(self, subProto);
    }
}

Used as follows:

class Employee {
  constructor (name, group) {
    useClassMethods(Employee, this, () => {
      this.name = name;
      this.setGroup(group);
    })
  }
  setGroup (group) {
    this.group = group;
  }
}

This seems to work, but the neutrons are pretty hot in this part of the reactor and I'd like to know if anybody else has a better solution or can pick holes in it.

srkleiman
  • 607
  • 8
  • 16
  • `this.setGroup(group);` should be `this.group = group;` as you are in constructor. Methods will be assigned after instance is created – Rajesh Oct 18 '22 at 04:24
  • The example is purposely contrived to illustrate the issue. – srkleiman Oct 18 '22 at 15:42
  • MDN points out that using `setPrototypeOf()` can reduce object performance. There might be a way to clone an object with an edited prototype chain, apply the function, and then remerge it back into the original but it seems dicey. – srkleiman Oct 19 '22 at 22:44
  • After reading [this](https://mathiasbynens.be/notes/prototypes) it seems like the main penalty of using `setPrototypeOf()` is invalidating the inline caches. This is not too bad during object construction, which happens once. Afterwards, the inline caches will be reestablished by normal use. `useClassMethods()` should not be used by non-constructor methods as it may impose a severe performance penalty. – srkleiman Oct 22 '22 at 23:11

2 Answers2

0

I came up with a better solution that uses a Proxy object instead of manipulating the object's prototype:

function useClassMethods (ctor, obj, fn) {
    const classMethods = Object.create(ctor.prototype);
    const proxy = new Proxy(obj, {
        get: (target, prop) => (
            Reflect.get(target.hasOwnProperty(prop) ? target : classMethods, prop)
        ),
    })
    fn.call(proxy);
}

super works in both the subclass and superclass. The only restriction is that fn must not be an arrow function so that it uses the passed this not the lexical this:

class Employee {
  constructor (name, group) {
    useClassMethods(Employee, this, function () {
      this.name = name;
      this.setGroup(group);
    })
  }
  setGroup (group) {
    this.group = group;
  }
}
srkleiman
  • 607
  • 8
  • 16
0

JavaScript has an OO quirk in that method calls in a superclass method will invoke subclass methods.

Yes. Don't do that in the constructor. (It's a problem in all OO languages, not just JS).

One solution is to only call internal methods in the superclass constructor (e.g. _setGroup()), and provide public wrappers that can be overriden in the child.

No need for that. You can directly call the superclass method from the superclass constructor:

class Employee {
  constructor (name, group) {
    this.name = name;
    Employee.prototype.setGroup.call(this, group);
  }
  setGroup (group) {
    this.group = group;
  }
}

however this amounts to the same as just doing

class Employee {
  constructor (name, group) {
    this.name = name;
    this.group = group;
  }
  setGroup (group) {
    this.group = group;
  }
}

which is preferable - one should simply not call overrideable methods from the constructor at all.
If you absolutely feel a need to run customiseable initialisation logic on your instances that the subclass constructor cannot handle by itself, consider this creation pattern.

Bergi
  • 630,263
  • 148
  • 957
  • 1,375
  • Thanks for this. The problem with your first suggestion is that calling `Employee.prototype.setGroup` can in turn call other overridable methods. My proposed solution handles that case. However, I agree that chaining is a better solution. – srkleiman Nov 11 '22 at 21:26