Source originale du contenu
In Prototypes are Objects, we saw that you can emulate “mixins” using Object.assign
on the prototypes that underly JavaScript “classes.” We’ll revisit this subject now and spend more time looking at mixing functionality into classes.
First, a quick recap: In JavaScript, a “class” is implemented as a constructor function and its prototype, whether you write it directly, or use the class
keyword. Instances of the class are created by calling the constructor with new
. They “inherit” shared behaviour from the constructor’s prototype
property.1
the object mixin pattern
One way to share behaviour scattered across multiple classes, or to untangle behaviour by factoring it out of an overweight prototype, is to extend a prototype with a mixin.
Here’s a class of todo items:
class Todo {
constructor (name) {
this.name = name || 'Untitled';
this.done = false;
}
do () {
this.done = true;
return this;
}
undo () {
this.done = false;
return this;
}
}
And a “mixin” that is responsible for colour-coding:
const Coloured = {
setColourRGB ({r, g, b}) {
this.colourCode = {r, g, b};
return this;
},
getColourRGB () {
return this.colourCode;
}
};
Mixing colour coding into our Todo prototype is straightforward:
Object.assign(Todo.prototype, Coloured);
new Todo('test')
.setColourRGB({r: 1, g: 2, b: 3})
//=> {"name":"test","done":false,"colourCode":{"r":1,"g":2,"b":3}}
We can “upgrade” it to have a private property if we wish:
const colourCode = Symbol("colourCode");
const Coloured = {
setColourRGB ({r, g, b}) {
this[colourCode]= {r, g, b};
return this;
},
getColourRGB () {
return this[colourCode];
}
};
So far, very easy and very simple. This is a pattern, a recipe for solving a certain problem using a particular organization of code.
functional mixins
The object mixin we have above works properly, but our little recipe had two distinct steps: Define the mixin and then extend the class prototype. Angus Croll pointed out that it’s more elegant to define a mixin as a function rather than an object. He calls this a functional mixin. Here’s Coloured
again, recast in functional form:
const Coloured = (target) =>
Object.assign(target, {
setColourRGB ({r, g, b}) {
this.colourCode = {r, g, b};
return this;
},
getColourRGB () {
return this.colourCode;
}
});
Coloured(Todo.prototype);
We can make ourselves a factory function that also names the pattern:
const FunctionalMixin = (behaviour) =>
target => Object.assign(target, behaviour);
This allows us to define functional mixins neatly:
const Coloured = FunctionalMixin({
setColourRGB ({r, g, b}) {
this.colourCode = {r, g, b};
return this;
},
getColourRGB () {
return this.colourCode;
}
});
enumerability
If we look at the way class
defines prototypes, we find that the methods defined are not enumerable by default. This works around a common error where programmers iterate over the keys of an instance and fail to test for .hasOwnProperty
.
Our object mixin pattern does not work this way, the methods defined in a mixin are enumerable by default, and if we carefully defined them to be non-enumerable, Object.assign
wouldn’t mix them into the target prototype, because Object.assign
only assigns enumerable properties.
And thus:
Coloured(Todo.prototype)
const urgent = new Todo("finish blog post");
urgent.setColourRGB({r: 256, g: 0, b: 0});
for (let property in urgent) console.log(property);
// =>
name
done
colourCode
setColourRGB
getColourRGB
As we can see, the setColourRGB
and getColourRGB
methods are enumerated, although the do
and undo
methods are not. This can be a problem with naïve code: we can’t always rewrite all the other code to carefully use .hasOwnProperty
.
One benefit of functional mixins is that we can solve this problem and transparently make mixins behave like class
:
const FunctionalMixin = (behaviour) =>
function (target) {
for (let property of Object.getOwnPropertyNames(behaviour))
Object.defineProperty(target, property, { value: behaviour[property] })
return target;
}
The above code supports methods with ordinary string names, but sometimes methods are declared with symbols (typically to create private methods). Although we won’t discuss that pattern yet, we can support those too:
const FunctionalMixin = (behaviour) =>
function (target) {
const instanceKeys = Object.getOwnPropertyNames(behaviour)
.concat(Object.getOwnPropertySymbols(behaviour));
for (let property of instanceKeys)
Object.defineProperty(target, property, { value: behaviour[property] })
return target;
}
Writing this out as a pattern would be tedious and error-prone. Encapsulating the behaviour into a function is a small win.
mixin responsibilities
Like classes, mixins are metaobjects: They define behaviour for instances. In addition to defining behaviour in the form of methods, classes are also responsible for initializing instances. But sometimes, classes and metaobjects handle additional responsibilities.
For example, sometimes a particular concept is associated with some well-known constants. When using a class, can be handy to namespace such values in the class itself:
class Todo {
constructor (name) {
this.name = name || Todo.DEFAULT_NAME;
this.done = false;
}
do () {
this.done = true;
return this;
}
undo () {
this.done = false;
return this;
}
}
Todo.DEFAULT_NAME = 'Untitled';
// If we are sticklers for read-only constants, we could write:
// Object.defineProperty(Todo, 'DEFAULT_NAME', {value: 'Untitled'});
We can’t really do the same thing with simple mixins, because all of the properties in a simple mixin end up being mixed into the prototype of instances we create by default. For example, let’s say we want to define Coloured.RED
, Coloured.GREEN
, and Coloured.BLUE
. But we don’t want any specific coloured instance to define RED
, GREEN
, or BLUE
.
Again, we can solve this problem by building a functional mixin. Our FunctionalMixin
factory function will accept an optional dictionary of read-only mixin properties, provided they are associated with a special key:
const shared = Symbol("shared");
function FunctionalMixin (behaviour) {
const instanceKeys = Object.getOwnPropertyNames(behaviour)
.concat(Object.getOwnPropertySymbols(behaviour))
.filter(key => key !== shared);
const sharedBehaviour = behaviour[shared] || {};
const sharedKeys = Object.getOwnPropertyNames(sharedBehaviour)
.concat(Object.getOwnPropertySymbols(sharedBehaviour));
function mixin (target) {
for (let property of instanceKeys)
Object.defineProperty(target, property, { value: behaviour[property] });
return target;
}
for (let property of sharedKeys)
Object.defineProperty(mixin, property, {
value: sharedBehaviour[property],
enumerable: sharedBehaviour.propertyIsEnumerable(property)
});
return mixin;
}
FunctionalMixin.shared = shared;
And now we can write:
const Coloured = FunctionalMixin({
setColourRGB ({r, g, b}) {
this.colourCode = {r, g, b};
return this;
},
getColourRGB () {
return this.colourCode;
},
[FunctionalMixin.shared]: {
RED: { r: 255, g: 0, b: 0 },
GREEN: { r: 0, g: 255, b: 0 },
BLUE: { r: 0, g: 0, b: 255 },
}
});
Coloured(Todo.prototype)
const urgent = new Todo("finish blog post");
urgent.setColourRGB(Coloured.RED);
urgent.getColourRGB()
//=> {"r":255,"g":0,"b":0}
mixin methods
Such properties need not be values. Sometimes, classes have methods. And likewise, sometimes it makes sense for a mixin to have its own methods. One example concerns instanceof
.
In earlier versions of ECMAScript, instanceof
is an operator that checks to see whether the prototype of an instance matches the prototype of a constructor function. It works just fine with “classes,” but it does not work “out of the box” with mixins:
urgent instanceof Todo
//=> true
urgent instanceof Coloured
//=> false
To handle this and some other issues where programmers are creating their own notion of dynamic types, or managing prototypes directly with Object.create
and Object.setPrototypeOf
, ECMAScript 2015 provides a way to override the built-in instanceof
behaviour: An object can define a method associated with a well-known symbol, Symbol.instanceOf
.
We can test this quickly:2
Coloured[Symbol.instanceOf] = (instance) => true
urgent instanceof Coloured
//=> true
{} instanceof Coloured
//=> true
Of course, that is not semantically correct. But using this technique, we can write:
const shared = Symbol("shared");
function FunctionalMixin (behaviour) {
const instanceKeys = Object.getOwnPropertyNames(behaviour)
.concat(Object.getOwnPropertySymbols(behaviour))
.filter(key => key !== shared);
const sharedBehaviour = behaviour[shared] || {};
const sharedKeys = Object.getOwnPropertyNames(sharedBehaviour)
.concat(Object.getOwnPropertySymbols(sharedBehaviour));
const typeTag = Symbol("isA");
function mixin (target) {
for (let property of instanceKeys)
Object.defineProperty(target, property, { value: behaviour[property] });
target[typeTag] = true;
return target;
}
for (let property of sharedKeys)
Object.defineProperty(mixin, property, {
value: sharedBehaviour[property],
enumerable: sharedBehaviour.propertyIsEnumerable(property)
});
mixin[Symbol.instanceOf] = (instance) => !!instance[typeTag];
return mixin;
}
FunctionalMixin.shared = shared;
urgent instanceof Coloured
//=> true
{} instanceof Coloured
//=> false
Do you need to implement instanceof
? Quite possibly not. “Rolling your own polymorphism” is usually a last resort. But it can be handy for writing test cases, and a few daring framework developers might be working on multiple dispatch and pattern-matching for functions.
summary
The charm of the object mixin pattern is its simplicity: It really does not need an abstraction wrapped around an object literal and Object.assign
.
However, behaviour defined with the mixin pattern is slightly different than behaviour defined with the class
keyword. Two examples of these differences are enumerability and mixin properties (such as constants and mixin methods like [Symbol.instanceof]
).
Functional mixins provide an opportunity to implement such functionality, at the cost of some complexity in the FunctionalMixin
function that creates functional mixins.
As a general rule, it’s best to have things behave as similarly as possible in the domain code, and this sometimes does involve some extra complexity in the infrastructure code. But that is more of a guideline than a hard-and-fast rule, and for this reason there is a place for both the object mixin pattern and functional mixins in JavaScript.