DEV Community

Cover image for The true prototypial nature beneath "JavaScript classes"
calvintwr
calvintwr

Posted on • Edited on

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

Top comments (7)

Collapse
 
jwp profile image
John Peters • Edited

This article and others like it in the JavaScript community remind me of how Java and C# people rejected JavaScript. Some rejected it for 10 years or more, until the ubiquitous nature of JavaScript forced everyone to learn it. Today's JavaScript is radically different from the first release. So many improvements have been made that almost everyone likes it now.

The JavaScript Prototype's Compositional Signature

This is the signature of a compositional pattern. It shows us there are three composites, a Person, a Prototype and a property. Composition is always good. But this particular code is busy, a full 32 chars. just to get to what the function does. I don't and never have liked the readability factor here.


Person.prototype.name = function() { return firstName + ' ' + lastName }
Person.prototype.greet = function() { ... }
Person.prototype.age = function() { ... }

Typescript Vaporizes Prototype Syntax!

... by automatically compiling all properties to prototypes. We never need to write the prototype syntax again!

Everything else in a Typescript compile are functions, including the "Class" construct if we target ESM5.

This means : free getter, setters, no use of Object.create, no diving to the obscured prototype layer, fantastic readability. If we don't like the 'new' or 'constructor' syntax, then we just skip them like this:

class Person {
  //Typescript makes these props prototypes
  firstName;
  lastName;
  birthYear;
  type;
  // Optional can be called
 //  But doesn't have to be implemented (it is over-rideable)
  fullName?() {
    return `${this.firstName} ${this.lastName}`;
  }
}

// Skip the new and constructor stuff
function UsingThePersonClass() {
  // Using Javascript object notation 
  // With Typescript Typing annotation
  // Automatic auto-completion of props. as we type 
  let obj: Person = {
    firstName: "first",
    lastName: "last",
    birthYear: 1976,
    type: "M",
  };
  // Return default result of fullName()
  return obj.fullName();
}

No need for interface definitions because a class is an interface with the added ability to initialize values! Simple, terse, clean and easy. Super readable!

In all honesty, the JavaScript Community's resistance to new approved standards within their own language is understandable. They are blinded to their first love as it was. The only problem now is that the first love is no longer there.

Collapse
 
calvintwr profile image
calvintwr • Edited

Nicely put, I can't agree with you more. The greatest relief in fact, if any, about the ES6 classes syntax, or the TypeScript syntax, is thankfully -- as you rightfully pointed out -- that you can still write simply and readably. Albeit it also can be turned into something really hardcore that looks half-intellectual and half-mangled. I really don't like the people who write such codes thinking that it's your fault you won't understand it.

So I guess I had two main points which is that readability must always be preserved. And yes related to the first love, still can't let go 😂 -- that it should have been proto instead of class, and create instead of new.

Wonderful comment, thanks.

Collapse
 
calvintwr profile image
calvintwr

Btw, your code gives the error of Cannot invoke an object which is possibly 'undefined'. on obj.fullName(). Any idea how to fix that?

Also, why do you need to wrap it in #UsingThePersonClass().

Thread Thread
 
Sloan, the sloth mascot
Comment deleted
 
jwp profile image
John Peters

The wrapping was just to show how to use the Person class.

Collapse
 
juanfrank77 profile image
Juan F Gonzalez

Thank you

This right here is, I think, my biggest pet-peeve with Angular and Typescript in general. We don't need new or class or super or any of that OOP stuff, JS is already a quite expressive and flexible language when used right.

Sadly, most people (myself included) come from a backend background. And some don't even try to learn & understand the language in the way it is.

Instead, they bring all the old customs and ways of thinking of languages like Java & C# and complain that JS is "weird" and "difficult to learn"

Collapse
 
calvintwr profile image
calvintwr • Edited

One of my perspectives spot on. There's the counter argument that some of these new, class or super helps to reduce code errors. Yes to a certain extent, but code that is difficult to read can lead to errors too, for one can make mistake if one is unable to fully understand. Ultimately whether plain old object literal/object.create syntax or ES6 classes, the idea is to write things simply.

And yes, I think Angular is sometimes over engineered.