JavaScript OOP Course 5: Composition (Mixins)
References
- Code with Mosh: The Ultimate JavaScript Mastery Series – Part 2
- W3School: JavaScript Tutorial
Composition Technique
With the Composition technique we can compose few objects together to create a new object. The Composition technique has a great flexibility. Here is how it works.
1. Let's define few objects that contains the methods (features) we need to mix-in at our application objects.
const canEat = {
eat: function() {
this.hunger--;
console.log('eating');
}
};
const canWalk = {
walk: function() {
this.hunger++;
console.log('walking');
}
};
2. Now we can use these objects together to create a person that can .eat()
and .walk()
. In ES6 we can use the Object.assign()
method. As the first argument to that method we must pass an object, where the members (properties and methods) of the rest objects (passed as the rest arguments) will be copied.
const person = Object.assign({hunger: null}, canEat, canWalk);
person;
{hunger: null, eat: ƒ, walk: ƒ}
hunger: null
eat: ƒ ()
walk: ƒ ()
[[Prototype]]: Object
3. Create a constructor function by using this technique. Here we need to pass this
of the newly created objects to the Object.assign()
method.
function Person() {
this.hunger = null;
Object.assign(this, canEat, canWalk);
}
const person1 = new Person();
person1;
Person {hunger: null, eat: ƒ, walk: ƒ}
hunger: null
eat: ƒ ()
walk: ƒ ()
[[Prototype]]: Object
Or we can assign the methods .eat()
and .walk()
at the Prototype level of our constructor function, which is better because will reduce the resources used by our program – the methods will be available at the prototype level and not be replicated at each single instance of the object.
function Person() {
this.hunger = null;
}
Object.assign(Person.prototype, canEat, canWalk);
const person2 = new Person();
person2;
Person {hunger: null}
hunger: null
[[Prototype]]: Object
eat: ƒ ()
walk: ƒ ()
constructor: ƒ Person()
[[Prototype]]: Object
4. Let's imagine tomorrow we need to add two new objects to our application – Goldfish and Dog, and both of them should have capability to swim.
const canSwim = {
swim: function() {
this.hunger++;
console.log('swi');
}
};
Then we can define new constructors.
function Dog() {
this.hunger = null;
}
Object.assign(Dog.prototype, canEat, canWalk, canSwim);
const dog1 = new Dog();
dog1;
Dog {hunger: null}
hunger: null
[[Prototype]]: Object
eat: ƒ ()
swim: ƒ ()
walk: ƒ ()
constructor: ƒ Dog()
[[Prototype]]: Object
function Goldfish() {
this.hunger = null;
}
Object.assign(Goldfish.prototype, canEat, canSwim);
const goldfish1 = new Goldfish();
goldfish1;
Goldfish {hunger: null}
hunger: null
[[Prototype]]: Object
eat: ƒ ()
swim: ƒ ()
constructor: ƒ Goldfish()
[[Prototype]]: Object
5. In order to make our code more readable we can extract the logic related to Object.assign()
in a function that use the rest and spread operator and is called mixin(target, …sources)
.
function mixin(targetConstructor, ...sources) { // here '...' is the rest operator
Object.assign(targetConstructor.prototype, ...sources); // here '...' is the spread operator
}
Now we can use the mixin(target, …sources)
function to simplify our code in the following way.
function Dog() { this.hunger = null; }
mixin(Dog, canEat, canWalk, canSwim);
function Goldfish() { this.hunger = null; }
mixin(Goldfish, canEat, canSwim);