This content originally appeared on raganwald.com and was authored by Reginald Braithwaite
In Functional Mixins, we discussed mixing functionality into JavaScript classes, changing the class. We observed that this has pitfalls when applied to a class that might already be in use elsewhere, but is perfectly cromulent when used as a technique to build a class from scratch. When used strictly to build a class, mixins help us decompose classes into smaller entities with focused responsibilities that can be shared between classes as necessary.
Let’s recall our helper for making a functional mixin. We’ll just call it mixin
:
function mixin (behaviour, sharedBehaviour = {}) {
const instanceKeys = Reflect.ownKeys(behaviour);
const sharedKeys = Reflect.ownKeys(sharedBehaviour);
const typeTag = Symbol('isa');
function _mixin (target) {
for (let property of instanceKeys)
Object.defineProperty(target, property, { value: behaviour[property] });
Object.defineProperty(target, typeTag, { value: true });
return target;
}
for (let property of sharedKeys)
Object.defineProperty(_mixin, property, {
value: sharedBehaviour[property],
enumerable: sharedBehaviour.propertyIsEnumerable(property)
});
Object.defineProperty(_mixin, Symbol.hasInstance, {
value: (i) => !!i[typeTag]
});
return _mixin;
}
This creates a function that mixes behaviour into any target, be it a class prototype or a standalone object. There is a convenience capability of making “static” or “shared” properties of the the function, and it even adds some simple hasInstance
handling so that the instanceof
operator will work.
Here we are using it on a class’ prototype:
const BookCollector = mixin({
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;
}
};
BookCollector(Person.prototype);
const president = new Person('Barak', 'Obama')
president
.addToCollection("JavaScript Allongé")
.addToCollection("Kestrels, Quirky Birds, and Hopeless Egocentricity");
president.collection()
//=> ["JavaScript Allongé","Kestrels, Quirky Birds, and Hopeless Egocentricity"]
mixins just for classes
It’s very nice that our mixins support any kind of target, but let’s make them class-specific:
function mixin (behaviour, sharedBehaviour = {}) {
const instanceKeys = Reflect.ownKeys(behaviour);
const sharedKeys = Reflect.ownKeys(sharedBehaviour);
const typeTag = Symbol('isa');
function _mixin (clazz) {
for (let property of instanceKeys)
Object.defineProperty(clazz.prototype, property, {
value: behaviour[property],
writable: true
});
Object.defineProperty(clazz.prototype, typeTag, { value: true });
return clazz;
}
for (let property of sharedKeys)
Object.defineProperty(_mixin, property, {
value: sharedBehaviour[property],
enumerable: sharedBehaviour.propertyIsEnumerable(property)
});
Object.defineProperty(_mixin, Symbol.hasInstance, {
value: (i) => !!i[typeTag]
});
return _mixin;
}
This version’s _mixin
function mixes instance behaviour into a class’s prototype, so we gain convenience at the expense of flexibility:
const BookCollector = mixin({
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;
}
};
BookCollector(Person);
const president = new Person('Barak', 'Obama')
president
.addToCollection("JavaScript Allongé")
.addToCollection("Kestrels, Quirky Birds, and Hopeless Egocentricity");
president.collection()
//=> ["JavaScript Allongé","Kestrels, Quirky Birds, and Hopeless Egocentricity"]
So far, nice, but it feels a bit bolted-on-after-the-fact. Let’s take advantage of the fact that Classes are Expressions:
const BookCollector = mixin({
addToCollection (name) {
this.collection().push(name);
return this;
},
collection () {
return this._collected_books || (this._collected_books = []);
}
});
const Person = BookCollector(class {
constructor (first, last) {
this.rename(first, last);
}
fullName () {
return this.firstName + " " + this.lastName;
}
rename (first, last) {
this.firstName = first;
this.lastName = last;
return this;
}
});
This is structurally nicer, it binds the mixing in of behaviour with the class declaration in one expression, so we’re getting away from this idea of mixing things into classes after they’re created.
But (there’s always a but), our pattern has three different elements (the name being bound, the mixin, and the class being declared). And if we wanted to mix two or more behaviours in, we’d have to nest the functions like this:
const Author = mixin({
writeBook (name) {
this.books().push(name);
return this;
},
books () {
return this._books_written || (this._books_written = []);
}
});
const Person = Author(BookCollector(class {
// ...
}));
Some people find this “clear as day,” arguing that this is a simple expression taking advantage of JavaScript’s simplicity. The code behind mixin
is simple and easy to read, and if you understand prototypes, you understand everything in this expression.
But others want a language to give them “magic,” an abstraction that they learn on the outside. At the moment, JavaScript has no “magic” for mixing functionality into classes. But what if there were?
class decorators
There is a well-regarded proposal to add Python-style class decorators to JavaScript in the next major revision after ECMAScript 2015.
A decorator is a function that operates on a class. Here’s a very simple example from the aforelinked implementation:
function annotation(target) {
// Add a property on target
target.annotated = true;
}
@annotation
class MyClass {
// ...
}
MyClass.annotated
//=> true
As you can see, annotation
is a class decorator, and it takes a class as an argument. The function can do anything, including modifying the class or the class’s prototype. If the decorator function doesn’t return anything, the class’ name is bound to the modified class.1
A class is “decorated” with the function by preceding the definition with @
and an expression evaluating to the decorator. in the simple example, we use a variable name.
Hmmm. A function that modifies a class, you say? Let’s try it:
const BookCollector = mixin({
addToCollection (name) {
this.collection().push(name);
return this;
},
collection () {
return this._collected_books || (this._collected_books = []);
}
});
@BookCollector
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;
}
};
const president = new Person('Barak', 'Obama')
president
.addToCollection("JavaScript Allongé")
.addToCollection("Kestrels, Quirky Birds, and Hopeless Egocentricity");
president.collection()
//=> ["JavaScript Allongé","Kestrels, Quirky Birds, and Hopeless Egocentricity"]
You can also mix in multiple behaviours with decorators:
const BookCollector = mixin({
addToCollection (name) {
this.collection().push(name);
return this;
},
collection () {
return this._collected_books || (this._collected_books = []);
}
});
const Author = mixin({
writeBook (name) {
this.books().push(name);
return this;
},
books () {
return this._books_written || (this._books_written = []);
}
});
@BookCollector @Author
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;
}
};
And if you want to use decorators to emulate Purely Functional Composition, it’s a fairly simple pattern:
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;
}
};
@BookCollector @Author
class BookLover extends Person {};
Class decorators provide a compact, “magic” syntax that is closely tied to the construction of the class. They also require understanding one more kind of syntax. But some argue that having different syntax for different things aids understandability, and that having both @foo
for decoration and bar(...)
for function invocation is a win.
using decorators
Decorators have not been formally approved, however there are various implementations available for transpiling decorator syntax to ES5 syntax. The examples in this post were evaluated with Babel.
If you prefer syntactic sugar that gives the appearance of a declarative construct, combining a mixin
function with [ES.later]’s class decorators does the trick.2
(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:
-
Although this example doesn’t show it, if it returns a constructor function, that is what will be assigned to the class’ name. This allows the creation of purely functional mixins and other interesting techniques that are beyond the scope of this post. ↩
-
By “ES.later,” we mean some future version of ECMAScript that is likely to be approved eventually, but for the moment exists only in transpilers like Babel. Obviously, using any ES.later feature in production is a complex decision requiring many more considerations than can be enumerated in a blog post. ↩
This content originally appeared on raganwald.com and was authored by Reginald Braithwaite
Reginald Braithwaite | Sciencx (2015-06-26T00:00:00+00:00) Using ES.later Decorators as Mixins. Retrieved from https://www.scien.cx/2015/06/26/using-es-later-decorators-as-mixins-2/
Please log in to upload a file.
There are no updates yet.
Click the Upload button above to add an update.