With the advent of the EcmaScript 6 standard (officially known as EcmaScript 2015), JavaScript has received a facelift with plenty of new features that will immediately look familiar to the Python developer. There are generator functions, array comprehensions, destructured arguments, formatted template strings, and by popular demand, a class
keyword!
It’s easy for a Python developer not well-versed in JavaScript to see new additions such as class definitions and think, “Cool. JS is finally a coherent programming language with right and proper object-oriented features”. She’ll replicate structures she’s familiar with, and happily code away. I’ll argue that while this familiarity can and will open the door to non-front-end developers, it could also become a major source of headaches, lost time, and code-rewrites.
JavaScript is interesting in that it’s a language that is very easy to pick up and tinker around with. It runs in any browser, and can be peppered into any web page without much thought. Once that same tinkerer attempts a non-trivial programming task such as a custom front-end web application, it becomes imperitive to learn the language’s internals fully, or risk debugging infuriatingly abstruse errors while simultaneously tangled up in a heap of code spaghetti.
Easily the most confusing and surprising of these internals for the uninitiated is JavaScript’s prototypal method of inheritance.
Notice, one subtle, yet crucial distinction in my language in the opening paragraph—I said “a class
keyword“, not “JavaScript now has classes!“ I’ll focus on that distinction below.
Let’s illustrate inheritance by using Python 3 to define a class Cowboy
that is a Person
:
1 2 3 4 5 6 7 8 9 10 11 12 | class Cowboy(Person):
def __init__(self, name, hat_color):
super().__init__(name)
self.hat_color = hat_color
def introduce(self):
introduction = super().introduce() #returns `"I'm " + self.name`
return "Howdy pardner, " + introduction
@classmethod
def get_sad_song(cls):
return "Every cowboy sings the same sad song."
|
And the same construct using the new semantics of EcmaScript 6:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | class Cowboy extends Person {
constructor(name, hatColor) {
super(name);
this.hatColor = hatColor;
}
introduce() {
var introduction = super.introduce(this); //returns `"I'm" + this.name`
return "Howdy pardner, " + introduction;
}
static getSadSong() {
return "Every cowboy sings the same sad song.";
}
}
|
Looks strikingly similar, doesn’t it? No Python developer, let alone any other kind of developer would have any trouble figuring out the relationship between Cowboy
and Person
.
Though it’s using the word, JavaScript does not really have “classes”. I promise. EcmaScript, in an outreach move, is trying to lull you into a nice cozy sense of familiarity. What you’re looking at is mostly sugar over this, the ES5 version:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | var Cowboy = function(name, hatColor) {
Person.call(this, name);
this.hatColor = hatColor;
};
Cowboy.prototype = Object.create(Person.prototype);
Cowboy.prototype.constructor = Cowboy;
Cowboy.prototype.introduce = function() {
var introduction = Person.prototype.introduce.call(this); //returns `"I'm" + this.name`
return "howdy pardner, I'm " + this.name;
};
Cowboy.getSadSong = function() {
return "Every cowboy sings the same sad song.";
};
|
A constructor function Cowboy
is created with initialization assignments as well as an explicit call to the parent “class” constructor using the newly-created instance as context.
That same Cowboy constructor function is assigned (functions are callable objects) its parent’s prototype, and subsequently given back its own constructor so that it can inherit from the parent and maintain its own identity, rather than simply becoming the parent. Afterwards, methods specific to the Cowboy object are added to its prototype. These steps are not intuitive (that’s why class
was introduced), but that’s how it’s fundamentally done.
Note that where we saw the super
call in the ES6 version, we see in ES5 an explicit invocation of the parent object’s method of the same name using this
, the instance as context. No magic here.
Crucial to an understanding of JavaScript inheritance is the fact that everything is an object. A JavaScript class that extends another is just an object created using that parent’s prototype, itself an object.
Though it is highly discouraged, at any point, a “class” instance’s prototype can be swapped out on the fly for another. Much more likely, and fiendishly difficult to diagnose and fix, an object further up in the chain of prototypal inheritance will have a method on its prototype tampered with, either by yourself, a team member, or even a library author. That change will immediately propagate downward and affect all objects in the chain, as they are not really subclassing or extending the object, but rather simply pointing to it at some point in the walk back up their inheritance chains. Objects acting as classes should be considered closed for manipulation, but this is just a convention, an unenforceable one that must be suggested by the code designer.
Often the complications that come from prototypal inheritance provide little benefit over other non-prototype, purely object-based solutions.
Let’s look at a class in Python to get a sense of where it might not make sense to replicate a class structure in full. Here is the source of Django’s DetailView class-based view:
1 2 3 4 5 6 | class DetailView(SingleObjectTemplateResponseMixin, BaseDetailView):
"""
Render a "detail" view of an object.
By default this is a model instance looked up from `self.queryset`, but the
view will support display of *any* object by overriding `self.get_object()`.
"""
|
It’s just using inheritance to combine two classes.
And the resulting method resolution:
DetailView
SingleObjectTemplateResponseMixin
TemplateResponseMixin
BaseDetailView
SingleObjectMixin
ContextMixin
View
Note there are two classes in that list not ending in ‘Mixin’: a BaseDetailView
, meant to be extended from by views concerned with displaying information about a single object; and View
, the bare-bones implementation of a class that can be utilized as a view.
As Django developers, most of the time when we are designing classes, we are doing so largely with mixins, base classes ideally acting as minimal collections of related properties and methods.
Though this certainly is inheritance at work, the taxonomy of the resulting class isn’t really thought of in a classical object-oriented-inheritance way—You wouldn’t really say that a DetailView
is a kind of SingleObjectTemplateResponseMixin
for instance. More likely, you recognize it as a View
composed of discreet pieces of logic that individually do things such as associate an object with the view and render a template to a response from the view.
This mixin-based design philosophy translates quite well into JavaScript object creation in replicating something similar to the Django view in JS (ES6, or otherwise). Rather than creating seven distinct classes, each with a prototype pointing up the chain, I’d argue we’d be much happier using object extension and composition instead.
A base constructor View
and its prototype could contain everything necessary for the bare-bones implementation. Everything additional would be implemented as a mixin object and composed with the base view as needed to create new “class” definitions.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | var View = function() {
// construtor initialization
};
View.prototype.dispatch = function() {
...
};
var ContextMixin = {
extraMethod: function() {}
};
var DetailView = function() {
// construtor initialization
};
// Using something like Underscore.js's _.extend function:
_.extend(DetailView.prototype, ContextMixin);
|
The chain has been reduced to two class-like objects which can have simple mixins containing new methods added to their prototypes.
This will make our lives a lot easier in diagnosing bugs when we only have to refer to (and potentially break) one prototype link up the chain rather than many. Bonus: not having to debug by inspecting myView.__proto__.__proto__.__proto__
. Missing, if done ES5-style as above, will be the convenience of using a super
call inside methods, though it isn’t hard to explicitly call what we need using call
and apply
where we need it. There is also no reason the prototype object-extension above can’t work in tandem with ES6 classes and super
as well.
Hopefully EcmaScript 6’s inclusion of the class
keyword won’t detract new JavaScript developers from getting familiar with prototypal inheritance and where it makes the most and least sense. JavaScript’s insistance on simply pointing objects to other objects can be tricky, yet liberating once that design philosophy is fully grasped.