DEV Community

Cover image for Using JavaScript classes without the `class` keyword
Corbin Crutchley for This is Learning

Posted on • Originally published at unicorn-utterances.com

Using JavaScript classes without the `class` keyword

Classes in JavaScript are both powerful and weird. While they allow us to create named objects with similarly purposed methods and properties, they're often misunderstood because of nuanced in the language itself.

But did you know that prior to 2015, JavaScript didn't even have a class keyword as part of the language?

Despite this, many programs at the time used classic Object Oriented Programming (OOP) methodologies such as using a class, extending it, and even adding static methods.

But without a class method, how did they even make classes?

A good question! Let's answer that and, along the way, look at:

  • How to create a "class" without the class keyword
  • How to "extend" a "class"
  • How to add static methods to our "class"

Create public fields with the contructor

Let's look at a modern JavaScript class:

class User {
    name = "Corbin",
    username = "crutchcorn",
    sayCatchphrase() {
        console.log("It depends");
    }
}
Enter fullscreen mode Exit fullscreen mode

This is a fairly basic class that has two properties (name and username) as well as a sayCatchphrase method.

However, despite the class keyword being added in 2015 with ES6, public fields like this weren't added until ECMAScript 2020:

A JavaScript compatibility table showing support for  raw `class` endraw  added in Node 6, but "Public fields" added in Node 12

So then how did classes get properties in years after 2015 but before 2020?

The answer? The constructor method:

class User {
    constructor() {
        this.name = "Corbin",
        this.username = "crutchcorn",
    }

    sayCatchphrase() {
        console.log("It depends");
    }
}
Enter fullscreen mode Exit fullscreen mode

In fact, using this constructor method, we can even add the method as well:

class User {
    constructor() {
        this.name = "Corbin",
        this.username = "crutchcorn",
        this.sayCatchphrase = function() {
            console.log("It depends");
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

An interesting fact, for sure - but it doesn't answer the question of how to make a class.

Don't worry, we're getting there!

Create a class without the class keyword

Before we answer the question of "how to make a class in JavaScript without the class keyword", let's take a step back and look at what a class is actually doing...

After all, a class like User above might create an object like so:

const userObject = {
    name: "Corbin",
    username: "crutchcorn",
    sayCatchphrase: function() {
        console.log("It depends");
    }
}
Enter fullscreen mode Exit fullscreen mode

Knowing this, we might think that the best way to make a class without the keyword is to return an object from a function:

function User() {
    return {
        name: "Corbin",
        username: "crutchcorn",
        sayCatchphrase: function() {
            console.log("It depends");
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

And sure enough, if we run this code using:

const user = new User();
user.sayCatchphrase(); // "It depends"
Enter fullscreen mode Exit fullscreen mode

It will run as-expected. However, it won't solve all cases. EG:

new User() instanceof User; // false
Enter fullscreen mode Exit fullscreen mode

Instead, what if we just converted the aforementioned class' constructor body to a function?:

function User() {
    this.name = "Corbin";
    this.username = "crutchcorn";
    this.sayCatchphrase = function() {
        console.log("It depends");
    }
}
Enter fullscreen mode Exit fullscreen mode

Now, not only do we have the method working, but instanceof works as well:

const user = new User();
user.sayCatchphrase(); // "It depends"
new User() instanceof User; // true
Enter fullscreen mode Exit fullscreen mode

Prototype Manipulation

But surely changing from a class to a function doesn't allow you to change the prototype in the same way?

Actually, it does! That's how this whole thing works!

Consider the following code:

function User() {
    this.name = "Corbin";
    this.username = "crutchcorn";
}

User.prototype.sayCatchphrase = function() {
   console.log("It depends");
}
Enter fullscreen mode Exit fullscreen mode

This is the same way of adding a method as the this.sayCatchphrase method as before, but is done by changing the prototype.

We can test this code still works by running:

const user = new User();
user.sayCatchphrase(); // "It depends"
Enter fullscreen mode Exit fullscreen mode

Create an extended class using the super method

Before we talk about function-based class extension, we need to talk about pre-ES2020 class creation once again.

See, when we convert the following code to use a contructor:

class Person {
    personality = "quirky";
}

class Corbin extends Person {
    name = "Corbin";
}
Enter fullscreen mode Exit fullscreen mode

Like so:

class Person {
    constructor() {
        this.personality = "quirky";
    }
}

class Corbin extends Person {
    constructor() {
        this.name = "Corbin";
    }
}
Enter fullscreen mode Exit fullscreen mode

And try to initialize it:

const corn = new Corbin()
Enter fullscreen mode Exit fullscreen mode

We get the following error:

Uncaught ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructor
    at new Corbin (<anonymous>:9:6)
Enter fullscreen mode Exit fullscreen mode

This is because we're not using the super() method to tell our extended class to utilize the parent's class' methods.

To fix this, we'll add that method to the extended class' constructor:

class Person {
    constructor() {
        this.personality = "quirky";
    }
}

class Corbin extends Person {
    constructor() {
        super();
        this.name = "Corbin";
    }
}
Enter fullscreen mode Exit fullscreen mode

Now our Corbin constructor work work as-intended:

const corn = new Corbin();
console.log(corn.name); // "Corbin";
console.log(corn.personality); // "quirky";
Enter fullscreen mode Exit fullscreen mode

Extend a functional class using Object.create

Let's now convert our Person and Corbin classes to use functions instead of the class keyword.

The person class is easy enough:

function Person() {
    this.personality = "quirky";
}
Enter fullscreen mode Exit fullscreen mode

And we could use the call method to bind Person's this to Corbin, like so:

function Corbin() {
    Person.call(this);
    this.name = "Corbin";
}
Enter fullscreen mode Exit fullscreen mode

And it appears to work at first:

const corn = new Corbin();
console.log(corn.name); // "Corbin";
console.log(corn.personality); // "quirky";
Enter fullscreen mode Exit fullscreen mode

But now, once again, if we call instanceof it doesn't support the base class:

new Corbin() instanceof Corbin; // true
new Corbin() instanceof Person; // false
Enter fullscreen mode Exit fullscreen mode

To fix this, we need to tell JavaScript to use the prototype of Person and combine it with the prototype of Corbin, like so:

function Person() {
}

Person.prototype.personality = "quirky";

function Corbin() {
}

Corbin.prototype = Object.create(Person.prototype);
Corbin.prototype.name = "Corbin";

const corn = new Corbin();
corn.personality // "quirky"
corn.name // "Corbin"

const pers = new Person();
pers.personality // "quirky"
pers.name // undefined
Enter fullscreen mode Exit fullscreen mode

Notice how we're using Object.create to create a base object from the other prototype

Static Methods

Let's wrap up this article by talking about how to add static methods to a functional class.

As a refresher, this is what a static method looks like on a ES2020 class:

class User {
    name = "Corbin",
    username = "crutchcorn",
    static sayCatchphrase() {
        console.log("It depends");
    }
}

User.sayCatchphrase(); // "It depends"
User.name // undefined

const corn = new User();
corn.name; // "Corbin"
Enter fullscreen mode Exit fullscreen mode

This can be added by providing a key to the function's name outside of the function body:

function User() {
    this.name = "Corbin",
    this.username = "crutchcorn",
}

User.sayCatchphrase() {
    console.log("It depends");
}


User.sayCatchphrase(); // "It depends"
User.name // undefined

const corn = new User();
corn.name; // "Corbin"
Enter fullscreen mode Exit fullscreen mode

Conclusion

This has been an interesting look into how to use JavaScript classes without the class keyword.

Hopefully, this has helped dispel some misunderstandings about how classes work in JavaScript or maybe just given historical context for why some code is written how it is.

Like learning JavaScript's fundamentals?

Check out my article that explains how to use the .bind keyword in JavaScript.

Read it and want more?

Check out my book that teaches the introduction of React, Angular, and Vue all at once; "The Framework Field Guide".

Until next time!

Top comments (16)

Collapse
 
efpage profile image
Eckehard

People are used to use all kind of "tricks" in Javascript.

    const sqr = (x) => x*x
    console.log(sqr(3))
Enter fullscreen mode Exit fullscreen mode

Everybody knows, this defines a function, but WHY do we use a language, that states to define a constant, but creates a function instead? Why does a language allow to use this kind of ambiguity?

There are a lot of good reasons, why things are a bit wiered in JS, but not everybody is happy to use a language like a shell game. Classes make Javascript more explicit. If you define a class using the CLASS-keyword, everybody will know your intention. This is often not the case with common Javascript code...

Collapse
 
crutchcorn profile image
Corbin Crutchley

I mean, I'm on board with the class keyword. This wasn't a slam piece, it was to help folks understand historical context of the language and maybe even understand the prototype system better

Collapse
 
efpage profile image
Eckehard

Of course, thank you very much for your effort. I just wanted to note, that it is not a advantage to define a class without using the keyword. Even if it was only "syntactical shugar" (which i think it is not), it is better to use a clear naming for things you do.

We should always be clear that a programming language is made for programmers, not for computers. A computer uses opcodes, so in that case we should use assembler.

Thread Thread
 
vsaulis profile image
Vladas Saulis

IMO computer languages are made for computers, - not for programmers. When you start realize it, programming will make easier.

Thread Thread
 
efpage profile image
Eckehard

Sy, why do we not use assembler? It is much easier to understand for a computer...

Thread Thread
 
vsaulis profile image
Vladas Saulis

Mostly we use heavy-weight frameworks which finally generate assembler/machine code for computers, not for you.

Collapse
 
baenencalin profile image
Calin Baenen

And the post did a great job illustrating that.

Collapse
 
joelbonetr profile image
JoelBonetR 🥇 • Edited

I'm inclined to think that the code example along the comment on the following paragraph is due to a common misunderstanding after seeing dozens of posts like "Do you prefer const or function?" because the only good answer to this is "it doesn't matter", let me explain why:

You can do:

const square = (x) => x*x;
Enter fullscreen mode Exit fullscreen mode

or

function square (x) { return x*x; }
Enter fullscreen mode Exit fullscreen mode

But they are not the same, technically.

1- const prevents reassignment of the reference (name) while function does not.
2- An arrow function doesn't have it's own lexical context, so it won't have a scoped this and can't be used as a constructor while function can be.
3- A const arrow function needs to be declared before calling it, otherwise it's undefined
4- A function can be declared after calling it.

Please note that you can also do:

const square = function(x) { return x*x; }
Enter fullscreen mode Exit fullscreen mode

Which both prevents reassignment and creates a lexical context, so it's (or it should be) a matter of -technical- needs on a given point on your software rather than a personal preference.

Now, as we should code for other humans to understand it, rather than just "the machine", the key here is in the naming guidelines, as a rule of thumb:

const myDog = 
Enter fullscreen mode Exit fullscreen mode

No verb, this should be a value.

const getUserById = 
Enter fullscreen mode Exit fullscreen mode

Does it have a verb? Yes, it should be a function then!

and please, no abbreviations nor acronyms on variable/const/function/class names please! 😂

Hope it helps,

Best wishes 😁

Collapse
 
vsaulis profile image
Vladas Saulis

There is no such primitive like 'class' in JS.

Collapse
 
efpage profile image
Eckehard

?

Thread Thread
 
vsaulis profile image
Vladas Saulis

There are predefined primitives in JS. And there is no primitive 'class'.

Collapse
 
vsaulis profile image
Vladas Saulis

Use 'var' - it's clearer.

Collapse
 
vsaulis profile image
Vladas Saulis • Edited

I always knew that classes in JS are just syntactic sugar.

Collapse
 
Collapse
 
vsaulis profile image
Vladas Saulis

I always convert JS classes into prototype chains without any consequences.

Collapse
 
wentout profile image
went • Edited

Please fix the error of re-assigning Corbin.prototype, instead of :

Corbin.prototype = Object.create(Person.prototype);
Enter fullscreen mode Exit fullscreen mode

much better do the following:

Object.setPrototypeOf(Corbin.prototype, Object.create(Person.prototype))
Enter fullscreen mode Exit fullscreen mode

Because otherwise Corbin instance is not a member of Corbin constructor if you will use instanceof for check...

Seems there is a non-enumerable .constructor property in Corbin.prototype, accordingly with the specification, which is then used for matching instanceof. And when you just re-assign ... you deleting it.

And for sure instead of code above you may also go like this:


function Person() {
}
Person.prototype.personality = "quirky";

function Corbin() {
}
Corbin.prototype = Object.create(Person.prototype, {
    constructor: { value: Corbin },
    name: { value: "Corbin" }
});

const corbin = new Corbin;
console.log(corbin instanceof Person); // true
console.log(corbin instanceof Corbin); // true

Enter fullscreen mode Exit fullscreen mode