The true prototypial nature beneath "JavaScript classes"

calvintwr

calvintwr

Posted on July 26, 2020

The true prototypial nature beneath "JavaScript classes"

I first wrote this stackoverflow answer in 2015. Obviously things has changed quite a bit, but still think there are much misdirections in JavaScript to address.

This article, as its title would suggest, will be contentious. But please, I am not about to say that we shouldn't use class and new. But to make that little dent, catch your attention, and hopefully we can all have some discussions over it.

Mainly is to explore, through a simple syntax, that Javascript is inherently classless, and its powerful prototypial nature obscured by class and new.

But, on balance, you have much to gain, and nothing to lose using ES6 classes (provided one writes it readably).

The point at the end of the day, please think of readability. The closer a language looks like to a human language, the better.

World without the "new" keyword.

And simpler "prose-like" syntax with Object.create().

First off, and factually, Javascript is a prototypal language, not class-based. The class keyword is in fact is just prototypial under the hood. Indulge me, and have a look at its true nature expressed in the simple prototypial form below, which you may come to see that is very simple, prose-like, yet powerful. I also will not use the prototype property, because I also find it rather unnecessary and complicated.

TLDR;

const Person = { 
    firstName: 'Anonymous',
    lastName: 'Anonymous',
    type: 'human',
    name() { return `${this.firstName} ${this.lastName}`},
    greet() { 
        console.log(`Hi, I am ${this.name()}.`)
    } 
}

const jack = Object.create(Person) // jack is a person
jack.firstName = 'Jack'            // and has a name 'Jack'
jack.greet()                       // outputs "Hi, I am Jack Anonymous."
Enter fullscreen mode Exit fullscreen mode

This absolves the sometimes convoluted constructor pattern. A new object inherits from the old one, but is able to have its own properties. If we attempt to obtain a member from the new object (#greet()) which the new object jack lacks, the old object Person will supply the member.

In Douglas Crockford's words: "Objects inherit from objects. What could be more object-oriented than that?"

You don't need constructors, no new instantiation (read why you shouldn't use new), no super, no self-made __construct, no prototype assignments. You simply create Objects and then extend or morph them.

This pattern also offers immutability (partial or full), and getters/setters.

TypeScript Equivalent

The TypeScript equivalent requires declaration of an interface:

interface Person { 
    firstName:  string,
    lastName: string,
    name: Function,
    greet: Function
}

const Person = { 
    firstName: 'Anonymous',
    lastName: 'Anonymous',
    name(): string { return `${this.firstName} ${this.lastName}`},
    greet(): void { 
        console.log(`Hi, I am ${this.name()}.`)
    } 
} 
const jack: Person = Object.create(Person)
Enter fullscreen mode Exit fullscreen mode

Creating an descendant/copy of Person

Note: The correct terms are prototypes, and their descendants/copies. There are no classes, and no need for instances.

const Skywalker    = Object.create(Person)
Skywalker.lastName = 'Skywalker'

const anakin       = Object.create(Skywalker)
anakin.firstName   = 'Anakin'
anakin.gender      = 'male' // you can attach new properties.

anakin.greet() // 'Hi, my name is Anakin Skywalker.'
Enter fullscreen mode Exit fullscreen mode

Let's look at the prototype chain:

/* Person --> Skywalker --> anakin */
Person.isPrototypeOf(Skywalker) // outputs true
Person.isPrototypeOf(anakin)    // outputs true
Skywalker.isPrototypeOf(anakin) // outputs true
Enter fullscreen mode Exit fullscreen mode

If you feel less safe throwing away the constructors in-lieu of direct assignments, fair point. One common way is to attach a #create method which you are read more about below.

Branching the Person prototype to Robot

Say when we want to branch and morph:

// create a `Robot` prototype by extending the `Person` prototype
const Robot = Object.create(Person)
Robot.type  = 'robot'
Robot.machineGreet = function() { console.log(10101) }

// `Robot` doesn't affect `Person` prototype and its descendants
anakin.machineGreet() // error
Enter fullscreen mode Exit fullscreen mode

And the prototype chain looks like:

/*
Person ----> Skywalker --> anakin
        |
        |--> Robot
*/
Person.isPrototypeOf(Robot) // outputs true
Robot.isPrototypeOf(Skywalker) // outputs false
Enter fullscreen mode Exit fullscreen mode

...And Mixins -- Because.. is Darth Vader a human or robot?

const darthVader = Object.create(anakin)

// for brevity, skipped property assignments 
// you get the point by now.

Object.assign(darthVader, Robot)

// gets both #Person.greet and #Robot.machineGreet
darthVader.greet() // "Hi, my name is Darth Vader..."
darthVader.machineGreet() // 10101
Enter fullscreen mode Exit fullscreen mode

Along with other odd things:

console.log(darthVader.type)     // outputs "robot".
Robot.isPrototypeOf(darthVader)  // returns false.
Person.isPrototypeOf(darthVader) // returns true.
Enter fullscreen mode Exit fullscreen mode

Which elegantly reflects the "real-life" subjectivity:

"He's more machine now than man, twisted and evil." - Obi-Wan Kenobi

"I know there is good in you." - Luke Skywalker

In TypeScript you would also need to extend the Person interface:

interface Robot extends Person {
    machineGreet: Function
}
Enter fullscreen mode Exit fullscreen mode

Conclusion

I have no qualms with people thinking that class and new are good for Javascript because it makes the language familiar and also provides good features. I use those myself. The issue I have is with people extending on the aforementioned basis, to conclude that class and new is just a semantics issue. It just isn't.

It also gives rise to tendencies to write the simple language of Javascript into classical styles that can be convoluted. Instead, perhaps we should embrace:

  1. class and new are great syntactic sugar to make the language easier to understand for programmers with class languages background, and perhaps allows a structure for translating other other languages to Javascript.
  2. But under the hood, Javascript is prototypial.
  3. And after we have gotten our head around Javascript, to explore it's prototypial and more powerful nature.

Perhaps in parallel, it should allow for a proto and create keyword that works the same with all the ES6 classes good stuff to absolve the misdirection.

Finally, whichever it is, I hoped to express through this article that the simple and prose-like syntax has been there all along, and it had all the features we needed. But it never caught on. ES6 classes are in general a great addition, less my qualm with it being "misleading". Other than that, whatever syntax you wish to use, please consider readability.

Further reading

Commonly attached #create method

Using the Skywalker example, suppose you want to provide the convenience that constructors brings without the complication:

Skywalker.create = function(firstName, gender) {

    let skywalker = Object.create(Skywalker)

    Object.assign(skywalker, {
        firstName,
        gender,
        lastName: 'Skywalker'
    })

    return skywalker
}

const anakin = Skywalker.create('Anakin', 'male')
Enter fullscreen mode Exit fullscreen mode

On #Object.defineProperty

For free getters and setters, or extra configuration, you can use Object.create()'s second argument a.k.a propertiesObject. It is also available in #Object.defineProperty, and #Object.defineProperties.

To illustrate its usefulness, suppose we want all Robot to be strictly made of metal (via writable: false), and standardise powerConsumption values (via getters and setters).

const Robot = Object.create(Person, {
    // define your property attributes
    madeOf: { 
        value: "metal",
        writable: false,
        configurable: false,
        enumerable: true
    },
    // getters and setters
    powerConsumption: {
        get() { return this._powerConsumption },
        set(value) { 
            if (value.indexOf('MWh')) {
                this._powerConsumption = value.replace('M', ',000k')
                return 
            }
            this._powerConsumption = value
            throw Error('Power consumption format not recognised.')
        }  
    }
})

const newRobot = Object.create(Robot)
newRobot.powerConsumption = '5MWh'
console.log(newRobot.powerConsumption) // outputs 5,000kWh
Enter fullscreen mode Exit fullscreen mode

And all prototypes of Robot cannot be madeOf something else:

const polymerRobot = Object.create(Robot)
polymerRobot.madeOf = 'polymer'
console.log(polymerRobot.madeOf) // outputs 'metal'
Enter fullscreen mode Exit fullscreen mode
💖 💪 🙅 🚩
calvintwr
calvintwr

Posted on July 26, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related