I'm currently working on a fairly JS-heavy project called Livewire. I'm not sure I've ever written more raw JavaScript (no framework), but overall I've enjoyed the experience.
However, I often miss language features from PHP. One of them is class traits (think mixins or includes). Traits are an easy way to organize related methods inside a class when a more robust refactoring isn't yet warranted.
You have a big class that isn't suited for refactoring to smaller classes, you just want to break it up into a few well-named files, and mix those into the main class.
Checkout this video on Laracasts for some compelling use cases (It just so happens that our podcast is featured in it!).
For this tutorial, we'll use the following class as our example.
// meal.js
class Meal {
hasMeat() {...}
hasPotatoes() {...}
scheduledTime() {...}
numberOfGuests() {...}
isNutritous() {...}
}
export default Meal
Now, let's say we want to group the related methods and extract them into separate files as mixins.
// meal.js
import ingredients from './ingredients'
import logistics from './logistics'
class Meal {
isNutritous() {...}
}
Object.assign(Meal.prototype, ingredients)
Object.assign(Meal.prototype, logistics)
export default Meal
// ingredients.js
export default {
hasMeat() {...},
hasPotatoes() {...},
}
// logistics.js
export default {
scheduledTime() {...},
numberOfGuests() {...},
}
First, let's look at Object.assign(subject, source)
. It takes the source
object and clones all of its properties to the subject
object. Think of it kinda like array_merge()
in PHP.
You may have seen it used for cloning JavaScript objects: var cloneOfFoo = Object.assign({}, foo)
In our example, we are mixing-in the properties from ingredients
and logistics
to Meal.prototype
. Let's dive into what Meal.prototype
means.
JavaScript is a "prototype-based language" as opposed to being "object-oriented". This is a deep subject, and you can get a full understanding here, but for our purposes, here's my crude, working definition:
Every object in JavaScript has a property called __proto__
which references the prototype
property of the class it was created from.
When a property is not found on an object, JavaScript looks for it in __proto__
, if it's not found on that, it looks for a __proto__
property on the __proto__
object and so on until it reaches the end of the chain.
Therefore, we can add properties to every instance of Meal
by adding the property to Meal.prototype
. In that sense, prototypes are like classes in other languages.
If you didn't follow that, here is it in action:
var foo = {}
// Note:Every JavaScript object eventually "inherits" from the Object class
Object.prototype.bar = 'baz'
foo.bar // returns "baz"
foo.__proto__ // returns {bar: "baz", ...}
Using Object.assign
is suitable for most cases, however, there is one caveat. It will not copy over getters from the source object to the target. It will evaluate them and copy the result. Let me show you what I mean:
class Baz {
constructor() {
this.foo = 'bar'
}
get lengthOfFoo() {
return this.foo.length
}
}
If we wanted to "mixin" lengthOfFoo
using Object.assign
we would get unexpected behavior:
Object.assign(Baz, { get lengthOfFoo() { return this.foo.length} })
will throw an error Cannot read property "length" of undefined
.
This is because Object.assign
evaluates getters and then sets them as properties on the new object, instead of copying the getters over.
If you want to use getters inside your traits/mixins, you need to use a different function other than Object.assign
. Here is a function that will copy over getters and setters (mostly) provided by Mozilla:
function addMixin(target, ...sources) {
sources.forEach(source => {
let descriptors = Object.keys(source).reduce((descriptors, key) => {
descriptors[key] = Object.getOwnPropertyDescriptor(source, key);
return descriptors;
}, {});
Object.getOwnPropertySymbols(source).forEach(sym => {
let descriptor = Object.getOwnPropertyDescriptor(source, sym);
if (descriptor.enumerable) {
descriptors[sym] = descriptor;
}
});
Object.defineProperties(target, descriptors);
});
return target;
}
Now, you can run addMixin(Baz, { get lengthOfFoo() { return this.foo.length} })
and you will get the expected result.
Hopefully, for those used to working with objects in PHP, this technique grants you another familiarity in JavaScript. Sometimes traits/mixins are the perfect abstraction, and I'm glad it's possible in JavaScript.
Thanks for tuning in! Caleb
I send out an email every so often about cool stuff I'm working on or launching. If you dig, go ahead and sign up!