How to Fix the ES6 `class` keyword
Nothing is perfect in the first version, but that doesn’t mean it’s hopeless. Maybe we can fix `class` in ES7
TL;DR Because of associated dangers with them (which have caused a lot of real damage to a lot of real projects) we need the ability to disallow `new`, `extends`, `super`, & `instanceof` in our lint rules. ES6 class won’t let us disallow `new` without throwing errors.
Now that it’s out there and people are using it, it seems like the only logical way forward is to try to make it better for everybody.
There is a whole section on object construction in the GoF “Design Patterns” book which exists only to get around the limitations of constructors and classes.
See also: Three Different Kinds of Prototypal OO.
The bottom line: Class doesn’t give you any power that isn’t already supplied by factory functions and the prototypal OO built into the language. All you’re doing when you create a class is opting into a less powerfull, less flexible mechanism and a whole lot of pitfalls and pain.
Class is a virus
Is there any hope that the `class` keyword will ever be useful? Maybe.
Why should we bother?
Why don’t we just create a lint rule and move on?
Classes could be useful. What if we want to build abstractions on top of it? What if we want to do more things in the language itself that could benefit with `class` integration (such as built-in traits)?
How to Fix `class`
Make class inheritance compositional
Similar to the way stamps are composed. In other words, change the behavior of `extends`, or deprecate `extends` and replace it with something like a `compose` keyword that can compose any number of classes.
There’s already discussion of doing this in ES7, (via traits, probably) and it’s really important that we get it right, and that whatever solution we come up with is call-site compatible with factory functions so that you can easily switch implementations.
As far as I know, even though refactoring to factory functions is fairly common in application development projects, factory substitution transparency is not a high priority on TC-39. Please correct me if I’m wrong, with a link to a reference.
The `new` keyword violates both the substitution principle and the open / closed principle. It’s also destructive because it adds zero value to the language, and it couples all callers to the details of object instantiation.
If you start with a class that requires `new` (all classes in ES6) and you later decide you need to use a factory instead of a class, you can’t make the change without refactoring all callers. Take a look at this example gist.
This is especially problematic for shared libraries and public interfaces, because you may not have access to all of the code using the class. You may think it doesn’t do any harm to just call a factory function with the `new` keyword, but the `new` keyword triggers behaviors that change what’s going on when the function is invoked. Certain kinds of factories are difficult to implement with `new`. See the gist for a more in-depth explanation.
Update: According to Brendan Eich, a fix for the `new` issue is being discussed for ES7:
An error caught by a throw is better than one that escapes as a silent-but-deadly (never mind allocation weight) difference in runtime semantics.
In case it helps, the idea mooted for ES7 is that you’d add a “call handler” to the class for when it is invoked without `new`:
I used `this.constructor`, but of course using `Point2D` directly is possible. In that case, subclasses would have to override the `[Symbol.call]` method, which seems undesirable and easily avoided as shown above.
Bottom line, we don’t want an implicit call handler in ES6. We need to get this right in ES7, and failing hard for now is the only way to be future-proof.
Make sure that `class` obeys the substitution principle
It’s vital that `class` obeys the substitution principle when you switch from a class to a factory. This is an important point. If callers are counting on any behavior or property of a class and you decide to change the implementation to a factory, you’ve just broken the calling code. I’m not aware of any good reason to switch from a factory to a class.
Decomposing large class hierarchies into smaller, more composable factories is a common maintenance activity on teams building large applications (100kloc+ codebases). The fact that you can’t simply substitute a factory for a class wastes a whole lot of time and energy, and it’s a major resource drain on a lot of startups. How do I know? I’ve consulted on dozens of those projects and small startups and enterprise organizations alike.
Believe me when I tell you: It’s fairly common, and very costly. As it stands today, companies are forced to refactor not just the class implementation, but all the call sites — sometimes the classes are in libraries and public SDKs that require significant coordination, evangelism and education. I’ve seen this specific problem kill projects and bring startups to their knees.
Super by definition tightly couples child classes to the parent class, which means any change in the parent class could cause unknowable rippling changes that can break anything that inherits from the parent class.
I once had to debug an issue with a subclass 6-levels deep in a class hierarchy. My coworkers were subclassing Backbone extend with wreckless abandon. I had to step line-by-line through every subview, following the control flow up the `super` chain. Finally, I found the problem in the top-level base class and fixed it. And then I spent the following hours fixing all the subclasses that broke when I fixed the behavior of the base class.
If you’re exporting a library class that people can inherit from, and you make any change to the behavior of the parent class, you’ve potentially broken a subclass that you don’t even know exists.
This is especially troubling because subclasses do not obey encapsulation rules. They frequently do things like call `super`, use and override private properties and methods, etc… Basically, super is a code smell, and a recipe for disaster.
It also doesn’t work with factories, which means it violates the substitution principle.
If we can’t fix these problems with `class`, we should push to deprecate the keyword entirely, because as it exists today, `class` is broken and absolutely should not be used.
Catalog of substitution breaks
The problem of substitution is quite serious, and needs to be fixed.
If we can catalog every possible breaking change here, there may be some hope for `class` in the future, assuming we can get consistency baked into the language spec.
- The behavior of `this`. It always refers to the new instance in a class constructor. In a factory function, `this` is dynamic, and follows a completely different set of rules. Possible solution: deprecate `this` and instead refer to the class or function by name. A major drawback of this solution is that it would break `.call()`, `.apply()` and `.bind()`, unless we also change their behavior to override the function name reference. I don’t like these solutions. Feel free to chime in with better options.
- `instanceof`— IMO, `instanceof` is broken anyway because it doesn’t do what the name describes, and from a user’s perspective, it flat out lies when you try to use it across execution contexts, or when the constructor prototype property changes. Possible solution: deprecate `instanceof`.
- `super` — `super` obviously can’t work for factory functions because inheritance works in a fundamentally different way with factories (the returned instance can be literally anything), and all users should adhere to the public API. Possible solution: deprecate `super`, and good riddance.