DEV Community

Cover image for 🚀How JavaScript Works (Part 10)? Class
Sam Abaasi
Sam Abaasi

Posted on

🚀How JavaScript Works (Part 10)? Class

JavaScript classes have become a fundamental part of modern JavaScript development. Introduced in ES6, they provide a convenient way to create and manage objects and their behaviors. However, understanding the full potential of JavaScript classes, beyond their syntactic sugar, is essential for writing efficient and maintainable code. In this article, we'll delve into JavaScript classes. We'll explore the syntax, inheritance, and the nuanced aspects of working with classes. We'll also discuss best practices, including how to avoid common pitfalls. Additionally, we'll take a closer look at the concept of auto-binding methods, which aims to address the challenge of maintaining the correct this context when working with class methods. Let's dive in!

Table of Contents

The Class Syntax

In JavaScript, classes are defined using the class keyword. They can have names, but it's worth noting that classes can also be expressions and even anonymous. The class syntax supports the declaration of constructors and methods without the need for commas between them. For example:

class Workshop {
    constructor(topic) {
        this.topic = topic;
    }

    ask(question) {
        console.log(`Welcome to the ${this.topic} workshop! ${question}`);
    }
}

const deepJS = new Workshop("JavaScript");
deepJS.ask("What happened to 'this'?");

Enter fullscreen mode Exit fullscreen mode

Extending Classes

JavaScript classes support inheritance through the extends clause. When a class extends another class, it inherits the methods and properties of the parent class. You can also define additional methods in the child class. Here's an example:

class ChildWorkshop extends Workshop {
    speakUp() {
        console.log(`Speaking up in the ${this.topic} workshop!`);
    }
}

const childJS = new ChildWorkshop("Advanced JavaScript");
childJS.ask("What's the superclass?");
childJS.speakUp();

Enter fullscreen mode Exit fullscreen mode

Relative Polymorphism

Relative polymorphism is a concept that allows child classes to override methods defined in the parent class. JavaScript's class system supports this through the super keyword. It enables child classes to refer to methods in the parent class with the same name. This is particularly useful for customizing behavior. For instance:

class ChildWorkshop extends Workshop {
    speakUp() {
        super.ask("Can I ask questions?");
        console.log(`Speaking up in the ${this.topic} workshop!`);
    }
}

Enter fullscreen mode Exit fullscreen mode

Beyond Syntactic Sugar: A Closer Look

Preserving this

One of the significant points is the behavior of this within class methods. In JavaScript, the this keyword is not auto-bound to class methods, and they behave just like regular functions. This means that if you pass a class method to a function like setTimeout, it will lose its this binding. To preserve this, developers often use hardbound methods or arrow functions, which can lead to unnecessary complexity and performance overhead.

Understanding Prototype

The class system in JavaScript is built upon the concept of prototypes. Methods and properties are defined on the prototype, not on instances. However, when you assign a function directly to an instance, it no longer exists on the prototype. This practice can lead to creating separate copies of functions for each instance, which is inefficient and diverges from the core principles of class-based JavaScript.

The Challenge of Maintaining this Context

One of the common challenges developers face when working with JavaScript classes is ensuring that the this context remains consistent when calling class methods. This is particularly crucial when you pass class methods as callbacks or use them in asynchronous operations like setTimeout. Without proper binding, the this context can become unpredictable.

Here's a simple example that illustrates this issue:

class Workshop {
  constructor(teacher) {
    this.teacher = teacher;
  }

  ask(question) {
    console.log(`${this.teacher} asked: ${question}`);
  }
}

const deepJS = new Workshop("Kyle");
const askFunction = deepJS.ask;

askFunction("What is auto-binding?");

Enter fullscreen mode Exit fullscreen mode

In this example, when we call askFunction, the this context inside the ask method is no longer bound to the deepJS instance. It results in an error because this.teacheris undefined.

The Concept of Auto-Binding

Auto-binding methods refer to a mechanism that automatically maintains the correct this context for class methods, without the need for explicit binding. While JavaScript doesn't provide native auto-binding, developers often come up with solutions to achieve it.

Here's an example of how you can implement a basic auto-binding utility for class methods:

function autoBindMethods(classInstance) {
  const prototype = Object.getPrototypeOf(classInstance);
  const methodNames = Object.getOwnPropertyNames(prototype);

  methodNames.forEach((methodName) => {
    if (typeof classInstance[methodName] === 'function') {
      classInstance[methodName] = classInstance[methodName].bind(classInstance);
    }
  });
}

class Workshop {
  constructor(teacher) {
    this.teacher = teacher;
    autoBindMethods(this); // Automatically bind class methods
  }

  ask(question) {
    console.log(`${this.teacher} asked: ${question}`);
  }
}

const deepJS = new Workshop("Kyle");
const askFunction = deepJS.ask;

askFunction("What is auto-binding?");

Enter fullscreen mode Exit fullscreen mode

In this modified example, we use the autoBindMethods function to automatically bind all class methods when an instance is created. This way, the ask method maintains the correct this context even when used as askFunction.

The Problem with Auto-Binding Methods

The Hacky Solution

In discussions about auto-bound methods, a potential solution has been suggested. The idea is to replace actual methods on class prototypes with getters. These getters would dynamically create hard-bound versions of the methods on the fly and cache them in a WeakMap. When you access a method through the getter, you automatically get a hard-bound version. This is a complex and unconventional approach that fundamentally changes the behavior of JavaScript functions.

Violating JavaScript's DNA

JavaScript functions are known for their dynamic nature. Auto-binding methods, while seemingly convenient, contradict the fundamental principles of JavaScript's functions. Attempting to force JavaScript into the mold of classes from other languages leads to complex and hacky solutions like the one described above. It doesn't align with JavaScript's core philosophy of flexibility and dynamic behavior.

The Danger of Permanent Hacks

As has been wisely noted, "There's nothing more permanent than a temporary hacker." The danger of introducing such hacky solutions is that they might become permanent parts of the language. Once developers start using them, it's challenging to reverse the trend, even if they contradict the core principles of the language.

Best Practices: Embracing the Full Power of Classes

To make the most of JavaScript classes, it's crucial to embrace their full capabilities. Here are some best practices to consider:

Leverage Prototypes: Embrace the prototype chain and avoid assigning functions directly to instances. This ensures efficient memory usage and maintains the dynamic nature of classes.

Avoid Hardbound Methods: While hardbound methods and arrow functions can preserve this, they might not be necessary. Embrace the dynamic nature of JavaScript and leverage this without overcomplicating your code.

Keep Classes Dynamic: Don't limit the class system's dynamic flexibility. If you need dynamic, flexible structures, consider alternatives like the module pattern, which has been available for over two decades and provides a robust solution for many use cases.

Conclusion

JavaScript classes are a powerful tool for creating and managing objects in your code. While they provide a convenient syntax, it's essential to understand their inner workings and nuances fully. In this article, we explored the syntax of JavaScript classes, their inheritance mechanism, and the concept of relative polymorphism using the super keyword. We also delved into the discussion surrounding auto-binding methods, which aim to address the challenge of maintaining the correct this context in class methods.

It's important to note that JavaScript's class system is built upon the concept of prototypes, where methods and properties are defined on the prototype rather than directly on instances. To make the most of JavaScript classes, we recommend leveraging prototypes, avoiding hardbound methods or arrow functions when they are unnecessary, and keeping classes dynamic.

While the idea of auto-binding methods has been considered, it's important to avoid overly complex solutions that contradict the core principles of JavaScript's dynamic behavior. By embracing the dynamic nature of JavaScript's class system and maintaining the flexibility it offers, you can write more efficient, maintainable, and robust JavaScript code.

Understanding JavaScript classes beyond their syntactic sugar empowers developers to make the most of this fundamental feature, creating better software and enhancing the JavaScript ecosystem.

Sources

Kyle Simpson's "You Don't Know JS"
MDN Web Docs - The Mozilla Developer Network (MDN)

Top comments (2)

Collapse
 
srbhr profile image
Saurabh Rai

You have written a really nice on going series @samanabbasi

Collapse
 
samabaasi profile image
Sam Abaasi

Thank you 💐💐