How does one avoid naming collisions from class inheritance in ES6 Javascript?
Large ES6 Javascript applications use a lot of inheritance, so much so that using generic names in a base class can mean debugging headaches later when creating derived classes. This can be a product of poor class design but seems more to be a problem with Javascript being able to scale up smoothly. Other languages provide mechanisms to hide inherited variables (Java) or properties (C#). Another way to alleviate this problem is to use private variables which isn't something Javascript has.
Here's an example of such a collision. A TreeObject class extends an Evented object (to inherit evented functionality) yet they both use parent to store their parent.
class Evented {
constructor(parent) {
this.parent = parent;
}
}
class TreeObject extends Evented{
constructor(eventParent, treeParent) {
super(eventParent);
this.parent = treeParent;
}
}
While this example is a bit artificial, I've had similar collisions in large libraries like Ember where terminology between the library and the end application overlap quite a bit causing me hours wasted here and there.
This really seems to be a design issue (use smaller objects and flatter hierarchies), but there's a solution to your problem as well: symbols!
const parentKey = Symbol("parent");
export class Evented {
constructor(parent) {
this[parentKey] = parent;
}
}
import {Evented} from "…"
const parentKey = Symbol("parent");
class TreeObject extends Evented {
constructor(eventParent, treeParent) {
super(eventParent);
this[parentKey] = treeParent;
}
}
They will reliably prevent any collisions, as all symbols are unique, regardless of their descriptors.
Large ES6 JavaScript applications use a lot of inheritance.
Actually, most of them don't. In a framework like Angular, one level of inheritance from the platform-defined classes is most common. Deep inheritance structures are brittle and best avoided. A large app might have some cases of two user levels of classes, A > B, where A contains a bunch of common logic and B1 and B2 are light specializations, such as a different template for a component, at a level that does not give rise to concerns about collisions.
Your example of Evented is semantically not really a parent class; it's more in the nature of a mixin. Since JS doesn't really handle mixins well, instead of deriving Tree from Evented, I'd hold the evented object as a property:
class TreeObject {
constructor(eventParent, treeParent) {
this.evented = new Evented(eventParent);
this.parent = treeParent;
}
send(msg) { this.evented.send(msg); }
}
If you really want to design Evented for use as a mixin-like superclass, then it's the responsibility of the designer to make member variables as unique as possible, as in
export class Evented {
constructor(parent) {
this.eventedParent = parent;
}
}
or use a symbol, as proposed in another answer. Or, consider using a map:
const parents = new Map();
class Evented {
constructor(parent) {
parents.set(this, parent);
}
sendParent(msg) {
parents.get(this).send(msg);
}
}
I've had similar collisions in large libraries like Ember
I haven't. Ember classes typically define few members, and the methods they define are well-defined and well-known.
Of course, the real solution is to use a typing system such as TypeScript. It does provide private members/methods. If a method does need to be public, TS will not let you define a method by the same name on the subclass unless the signatures match.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With