Caleb Porzio

Equivalent of PHP Class Traits in JavaScript

Mar 2019

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.

The Problem

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.

The Solution

// 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() {...},
}

What's happening here

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", ...}

Cavaet

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.

Summing up

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


My Newsletter

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!