Investigating JavaScript Objects and Classes

In my last post, I compared a Rust struct and impl block to a Java class as a way to associate the initially odd Rust syntax with something more recognizable. I wanted to provide an example of something similar in JavaScript, but I couldn't decide what to show. The class syntax is relatively new, and many respected developers in the JavaScript community seem to prefer using other methods. Do I use some form of Object.create even though it doesn't look much like the Class syntax many people are used to? How about the old function constructor syntax? With so many different options, I decided to punt the issue to a post of it's own.

In JavaScript, objects are just bags of properties. Properties can be strings, numbers, arrays, functions, other objects, etc, and it is this apparent simplicity and flexibility that makes understanding them so difficult.

The simplest way to create an object is to throw some properties inside of a couple of brackets:

const rgb = {
    red: 100,
    green: 120,
    blue: 200,
    
    hexCode: function() {
        let rgb = [this.red, this.green, this.blue]
        return rgb.reduce((acc, x) => { 
            return x > 15 ? acc + x.toString(16) : acc + "0" + x.toString(16);
        }, "");
    }
};

> rgb.red
< 100

> rgb.hexCode()
< "6478c8"

Everything here is a property, even our methods are just properties with function values.

If we wanted to create a bunch of these objects, we could create a function that takes a red, green, and blue parameter and returns our object:

function RGB(r, g, b) {
    return {
        red: r,
        green: g,
        blue: b,

        hexCode: function() {
            let rgb = [this.red, this.green, this.blue]
            return rgb.reduce((acc, x) => { 
                return x > 15 ? acc + x.toString(16) : acc + "0" + x.toString(16);
            }, "");
        }
    }
}

> let rgb = RGB(100, 200, 250);

> rgb.red
< 100

> rgb.hexCode()
< "64c8fa"

This function creates the same object we created before, but allows us to make multiple instances of it with different values for red, green, and blue - much like a class. But are these objects we've created really different instances of RGB as we'd expect in a class based language? rgb instanceof RGB says false, but we'll come back to that later.

Until a few years ago, the closest we could get to a traditional class was a constructor function with our methods added as properties to the function's prototype:

function RGB(red, green, blue) {
    this.red = red;
    this.green = green;
    this.blue = blue;
}
RGB.prototype.hexCode = function() {
    let rgb = [this.red, this.green, this.blue]
        return rgb.reduce((acc, x) => { 
            return x > 15 ? acc + x.toString(16) : acc + "0" + x.toString(16);
        }, "");
}

> let rgb = new RGB(55, 105, 175);

> rgb.hexCode()
< "3769af"

Notice the new keyword here. To create an instance of an object from a constructor function, we have to precede the function call with new. This time if we check to see whether we are creating instances of RGB, rgb instanceof RGB = true.

As of ES6, we have the option to use the class keyword to create a much more familiar looking constructor function:

class RGB {
    constructor(red, green, blue) {
        this.red = red;
        this.green = green;
        this.blue = blue;
    }
    hexCode() {
        let rgb = [this.red, this.green, this.blue]
        return rgb.reduce((acc, x) => { 
            return x > 15 ? acc + x.toString(16) : acc + "0" + x.toString(16);
        }, "");
    }
}

> let rgb = new RGB(44, 232, 110);

> rgb.hexCode()
< "2ce86e"

We still use the new keyword to create a new instance of the object when we use the class syntax, and rgb instanceof RGB still = true. Despite it's looks, this isn't a true class as we think of it in Java. A JavaScript class is just a special type of function. We're still just playing with objects and prototypes.

Another possibility is to use a prototype object with a factory function. It looks something like this:

const protoRGB = {
    hexCode: function() {
        let rgb = [this.red, this.green, this.blue]
        return rgb.reduce((acc, x) => { 
            return x > 15 ? acc + x.toString(16) : acc + "0" + x.toString(16);
        }, "");
    }
}

function createRGB(red, green, blue) {
    let rgb = Object.create(protoRGB);
    rgb.red = red;
    rgb.green = green;
    rgb.blue = blue;
    return rgb;
}

> let rgb = createRGB(101, 202, 123);

> rgb.hexCode()
< "65ca7b"

Here we create a base object, much like our very first example, but with only the function properties. Within a function, we use Object.create on protoRGB to create a new object with protoRGB's methods as its prototype. Then we set any additional properties we need and return the new object - no need for new here. There is no way to use instanceof in this situation, however, neither rgb instanceof createRGB nor rgb instanceof protoRGB = true. Advocates of this style recommend it not only for its flexibility, but also because they believe the use of instanceof is deceptive and should be avoided. Check out Eric Elliott's JavaScript Scene - Factory Functions, Constructors, Classes and Kyle Simpson's You Don't Know JS: this & Object Prototypes for more information on the subject of using instanceof with objects and on JavaScript classes generally.

And yet, there's more

What if we want private or read only properties in our objects? JavaScript doesn't have a private keyword at the moment, so we have to use closures to hide information. Lets go back to our first example, the simple object. To mimic a private property we need a way to create some scope to close over. We can accomplish this with an IIFE (immediately invoked function expression):

const rgb = (function(r, g, b) {
    let red = r;
    let green = g;
    let blue = b;
    
    let rgb = {
        getRed: function() {
            return red;
        },
        getGreen: function() {
            return green;
        },
        getBlue: function() {
            return blue;
        },

        hexCode: function() {
            let rgb = [this.getRed(), this.getGreen(), this.getBlue()]
            return rgb.reduce((acc, x) => { 
                return x > 15 ? acc + x.toString(16) : acc + "0" + x.toString(16);
            }, "");
        }
    }
    return rgb;
}(100, 120, 200));

> red
< ReferenceError: red not defined

> rgb.red
< undefined

> rgb.getRed()
< 100

> rgb.hexCode()
< "6478c8"

We created another bag of properties here, but instead of placing our red, green, and blue properties inside of the object, we place them on the outside and use methods to access them. We can do this because red, green, and blue are a part of rgb's lexical scope - a sort of bubble around the environment in which rgb was defined - granting access to any information inside the scope to rgb, but insulating it from anything on the outside.

A modified version of the function from the second example makes it easier to visualize our closure bubble:

function RGB(red, green, blue) {
    let r = red;
    let g = green;
    let b = blue;
    return {
        getRed: function() {
            return r;
        },
        getGreen: function() {
            return g;
        },
        getBlue: function() {
            return b;
        },

        hexCode: function() {
            let rgb = [this.getRed(), this.getGreen(), this.getBlue()]
            return rgb.reduce((acc, x) => { 
                return x > 15 ? acc + x.toString(16) : acc + "0" + x.toString(16);
            }, "");
        }
    }
}

> let rgb = RGB(100, 22, 212);

> rgb.red
< undefined

> rgb.getRed()
< 100

> rgb.hexCode()
< "6416d4"

When we create the object rgb, rgb closes over all of the data within the function's scope and is able to access it. Any data inside the function, but outside of the return value is hidden from the outside. Keep in mind you don't have to create new variables to hold the values passed into the function as I've shown here. I've done this mainly for demonstration purposes and because it helps me visualize the closure. You could just as easily reference the parameters red, green, and blue from getRed, getGreen, and getBlue.

We can apply the same idea to the function constructor and class. For the sake of brevity, I'll only show the class syntax:

class RGB {
    constructor(red, green, blue) {
        let r = red;
        let g = green;
        let b = blue;
        this.getRed = function() {
          return r;
        };
        this.getGreen = function() {
          return g;
        };
        this.getBlue = function() {
          return b;
        }
    }

    hexCode() {
        let rgb = [this.getRed(), this.getGreen(), this.getBlue()]
        return rgb.reduce((acc, x) => { 
            return x > 15 ? acc + x.toString(16) : acc + "0" + x.toString(16);
        }, "");
    }
}

> let rgb = new RGB(12, 32, 99);

> rgb.red
< undefined

> rgb.getRed()
< 12

> rgb.hexCode()
< "0c2063"

The same goes for our factory implementation:

const protoRGB = {
    hexCode: function() {
        let rgb = [this.getRed(), this.getGreen(), this.getBlue()]
        return rgb.reduce((acc, x) => { 
            return x > 15 ? acc + x.toString(16) : acc + "0" + x.toString(16);
        }, "");
    }
}

function createRGB(red, green, blue) {
    let rgb = Object.create(protoRGB);
    let r = red;
    let g = green;
    let b = blue;
    rgb.getRed = function() { return r; }
    rgb.getGreen = function() { return g; }
    rgb.getBlue = function() { return b; }
    return rgb;
}

> let rgb = createRGB(1, 2, 3);

> rgb.red
< undefined

> rgb.getRed()
> 1

> rgb.hexCode()
> "010203"

It's conceivable I've gone overboard trying to show why I had difficulty deciding whether to include an example of a JavaScript class in my last post, but I'm still not certain what I should have shown as an "equivalent" JavaScript "class". I've only just scratched the surface of JavaScript objects and classes, and the nuance that differentiates them from traditional classes. Maybe it's better not to compare them to traditional classes at all.

Even if we don't compare JavaScript's style of object-oriented programming to other styles, what's the right way to do it in JavaScript? Chances are, the "right" way, varies depending on who you ask and where you are. I've seen all-star JavaScript developers champion the factory method over class, and I've seen incredibly successful companies like Airbnb advocate using the class method. Even if it seems like a large part of the community is gravitating towards using class at the moment, it can't hurt to understand different ways to approach the problem.

Be water, my friend.

Reference:
JavaScript Scene - Factory Functions, Constructors, Classes, You Don't Know JS: this & Object Prototypes, Eloquent Javascript: Ch6 - The Secret Life of Objects, CSS Tricks - JavaScript Constructors, AirBnB JavaScript Style Guide, IIFE