Prerequisite: This post presumes that readers are familiar with JavaScript’s objects, know how a prototype defines behaviour for an object, know what a constructor function is, and how a constructor’s .prototype
property is related to the objects it constructs. Passing familiarity with ECMAScript 2015 syntax will be helpful.
We have always been able to create a JavaScript class like this:
function Person (first, last) {
this.rename(first, last);
}
Person.prototype.fullName = function fullName () {
return this.firstName + " " + this.lastName;
};
Person.prototype.rename = function rename (first, last) {
this.firstName = first;
this.lastName = last;
return this;
}
Person
is a constructor function, and it’s also a class, in the JavaScript sense of the word “class.”
ECMAScript 2015 provides the class
keyword and “compact method notation” as syntactic sugar for writing a function and assigning methods to its prototype (there is a little more involved, but that isn’t relevant here). So we can now write our Person
class like this:
class Person {
constructor (first, last) {
this.rename(first, last);
}
fullName () {
return this.firstName + " " + this.lastName;
}
rename (first, last) {
this.firstName = first;
this.lastName = last;
return this;
}
};
Nice. But behind the scenes, you still wind up with a constructor function bound to the name Person
, and with Person.prototype
being an object that looks like this:
{
fullName: function fullName () {
return this.firstName + " " + this.lastName;
},
rename: function rename (first, last) {
this.firstName = first;
this.lastName = last;
return this;
}
}
prototypes are objects
If we want to change the behaviour of a JavaScript object, we can add, remove, or modify methods of the object by adding, removing, or modifying the functions bound to properties of the object. This differs from most classical languages, they have a special form (e.g. Ruby’s def
) for defining methods.
Prototypes in JavaScript are “just objects,” and since they are “just objects,” we can add, remove, or modify methods of the prototype by adding, removing, or modifying the functions bound to properties of the prototype.
That’s exactly what the ECMAScript 5 code above does, and the ECMAScript 2015 class
syntax “desugars” to equivalent code.
Prototypes being “just objects” means we can use any technique that works on objects on prototypes. For example, instead of binding functions to a prototype one-at-a-time, we can bind them en masse using Object.assign
:
function Person (first, last) {
this.rename(first, last);
}
Object.assign(Person.prototype, {
fullName: function fullName () {
return this.firstName + " " + this.lastName;
},
rename: function rename (first, last) {
this.firstName = first;
this.lastName = last;
return this;
}
})
And of course, we could use compact method syntax1 if we like:
function Person (first, last) {
this.rename(first, last);
}
Object.assign(Person.prototype, {
fullName () {
return this.firstName + " " + this.lastName;
},
rename (first, last) {
this.firstName = first;
this.lastName = last;
return this;
}
})
mixins
Since class
desugars to constructor functions and prototypes, we can mix and match techniques:
class Person {
constructor (first, last) {
this.rename(first, last);
}
fullName () {
return this.firstName + " " + this.lastName;
}
rename (first, last) {
this.firstName = first;
this.lastName = last;
return this;
}
};
Object.assign(Person.prototype, {
addToCollection (name) {
this.collection().push(name);
return this;
},
collection () {
return this._collected_books || (this._collected_books = []);
}
})
We have just “mixed” methods concerned with collecting books into our Person
class. It’s great that we can write code in a very “point-free” style, but naming things is also great:
const BookCollector = {
addToCollection (name) {
this.collection().push(name);
return this;
},
collection () {
return this._collected_books || (this._collected_books = []);
}
};
class Person {
constructor (first, last) {
this.rename(first, last);
}
fullName () {
return this.firstName + " " + this.lastName;
}
rename (first, last) {
this.firstName = first;
this.lastName = last;
return this;
}
};
Object.assign(Person.prototype, BookCollector);
We can do this as much as we like:
const BookCollector = {
addToCollection (name) {
this.collection().push(name);
return this;
},
collection () {
return this._collected_books || (this._collected_books = []);
}
};
const Author = {
writeBook (name) {
this.books().push(name);
return this;
},
books () {
return this._books_written || (this._books_written = []);
}
};
class Person {
constructor (first, last) {
this.rename(first, last);
}
fullName () {
return this.firstName + " " + this.lastName;
}
rename (first, last) {
this.firstName = first;
this.lastName = last;
return this;
}
};
Object.assign(Person.prototype, BookCollector, Author);
why we might want to use mixins
Composing classes out of base functionality (Person
) and mixins (BookCollector
and Author
) provides several benefits.
First, sometimes functionality does not neatly decompose in a tree-like form. Book authors are sometimes corporations, not persons. And antiquarian book stores collect books just like bibliophiles.
A “mixin” like BookCollector
or Author
can be mixed into more than one class. Trying to compose functionality using “inheritance” doesn’t always work cleanly.
Another benefit is not obvious from a toy example, but in production systems classes can grow to be very large. Even if a mixin is not used in more than one class, decomposing a large class into mixins helps fulfil the Single Responsibility Principle. Each mixin can handle exactly one responsibility. That makes things easier to understand, and much easier to test.
why this matters
There are other ways to decompose responsibilities for classes (such as delegation and composition), but the point here is that if we wish to use mixins, it is very simple and easy to do, because JavaScript does not have a large and complicated OOP mechanism that imposes a rigid model on programs.
In Ruby, for example, mixins are easy because a special feature, modules was baked into Ruby from the start. In other OO languages, mixins are difficult, because the class system does not support them and they are not particularly friendly to metaprogramming.
JavaScript’s choice to build OOP out of simple parts–objects, functions, and properties–makes the development of new ideas possible.
(discuss on hacker news)
more reading:
- Prototypes are Objects (and why that matters)
- Classes are Expressions (and why that matters)
- Functional Mixins in ECMAScript 2015
- Using ES.later Decorators as Mixins
- Method Advice in Modern JavaScript
super()
considered hmmm-ful- JavaScript Mixins, Subclass Factories, and Method Advice
- This is not an essay about ‘Traits in Javascript’
notes:
-
There are subtleties involving the
super
keyword to consider, but that is not the point of this article. ↩