Skip to content

Avoid JavaScript Classes

Posted on:June 24, 2023 at 11:11 AM

In JavaScript, a class is a blueprint for creating objects with specific properties and methods. It is a way to define a new type of object that can be instantiated multiple times with different values. They also allow for inheritance, meaning a class can inherit properties and methods from another. Here is an example of building a Counter using a JavaScript class.

class Counter {
  constructor() {
    this.count = 0;
  }

increment() {
    this.count++;
  }

decrement() {
    this.count--;
  }

getCount() {
    return this.count;
  }
}

const myCounter = new Counter();
myCounter.increment();
myCounter.increment();
myCounter.increment();
console.log(myCounter.getCount()); // Output: 3

myCounter.decrement();
console.log(myCounter.getCount()); // Output: 2

this is confusing

In JavaScript, this keyword can be a source of confusion for developers, especially those new to the language. The value of this depends on how a function is called and can be different in different contexts. It can lead to unexpected behavior and bugs if not used correctly.

One common source of confusion is when this is used inside a method of an object. In this case, this refers to the object itself. However, if the method is called without the object context, this will refer to the global object (in non-strict mode) or undefined (in strict mode).

Another source of confusion is when this is used inside a callback function. In this case, this may not refer to the object that the callback is attached to, but instead to the global object or some other object, depending on how the callback is called.

Understanding how this works and using it consistently and appropriately is important to avoid confusion. Here is an example of using this incorrectly.

class Counter {
 constructor() {
   this.count = 0;
 }
 
 increment() {
   this.count++;
 }

delayedIncrement() {
    setTimeout(function() {
      // incorrect use of this
      this.increment();
    }, 1000);
  }
}

const myCounter = new Counter();
myCounter.delayedIncrement(); // Output: "Uncaught TypeError: this.increment is not a function"

Techniques for using this

One common technique is to use the bind, call, or apply methods to set this value when explicitly calling a function. This approach works well when used with callbacks.

class Counter {
  constructor() {
    this.count = 0;
  }

increment() {
    this.count++;
  }

delayedIncrement() {
   const increment = this.increment.bind(this);
   setTimeout(function() {
     increment();
   }, 1000);
  }
}

Another technique is to use arrow functions, which do not have their own this value and instead inherit this from the surrounding context.

class Counter {
  constructor() {
    this.count = 0;
  }

increment() {
    this.count++;
  }

delayedIncrement() {
  setTimeout(() => {
    this.increment();
  }, 1000);
 }
}

Cautions to take with classes

When working with classes and this in JavaScript, there are a few cautions that developers should keep in mind to avoid confusion and errors:

  1. Be aware of how this works: this in JavaScript is a dynamic keyword that refers to the current execution context. In the case of classes, this refers to the current class instance. However, this can also be affected by how a function is called and can refer to the global object or undefined if not used correctly.
  2. Use arrow functions for class methods: Arrow functions do not have their own this value and instead inherit this from the surrounding context. This makes them a good choice for class methods, as they ensure that this always refers to the current instance of the class.
  3. Use bind, call, or apply to set this explicitly: If you need to call a function with a specific this value, you can use the bind, call, or apply methods to set this explicitly. This can be useful in cases where you need to pass a class method as a callback function to another function.
  4. Be careful when using destructuring: When you destructure a class method from an instance of the class, you lose the object context, which may not refer to the current instance of the class. To avoid this, you can call the method on the instance directly or use bind to bind this to the instance.
  5. Avoid using this in nested functions: When you define a function inside another function, this inside the nested function may not refer to the current instance of the class. To avoid this, you can use an arrow function for the nested function or use bind to bind this to the current instance.

By keeping these cautions in mind, you can avoid common issues with this when working with classes in JavaScript.

Alternative to classes

Using the factory pattern is an alternative to using classes in JavaScript and can provide several benefits over class-based object creation. The factory pattern involves creating objects through a factory function rather than through a constructor or class. The factory function is responsible for creating and returning new objects and can be used to customize the behavior of the objects based on input parameters.

One of the main benefits of using the factory pattern is that it provides more flexibility than class-based object creation. With the factory pattern, you can create objects with different behaviors based on input parameters without defining separate classes for each behavior. This can make your code more modular and easier to maintain, as you can reuse the same factory function to create different types of objects.

Another benefit of using the factory pattern is that it can help to avoid some of the confusion and errors that can arise when using classes in JavaScript. With the factory pattern, you don’t have to worry about the complexities of this and object context, as the factory function is responsible for creating and returning new objects with the correct context.

Here is an example of using the factory pattern to implement a counter:

function createCounter() {
  let count = 0;

  function increment() {
    count++;
    console.log(`Count is now ${count}.`);
  }

  function decrement() {
    count--;
    console.log(`Count is now ${count}.`);
  }

  function getCount() {
    console.log(`Count is ${count}.`);
    return count;
  }

  function delayedIncrement() {
    setTimeout(() => {
      increment();
    }, 1000);
  }

  return {
    increment,
    decrement,
    getCount,
    delayedIncrement
  };
}

const myCounter = createCounter();
myCounter.increment(); // Output: "Count is now 1."
myCounter.decrement(); // Output: "Count is now 0."
myCounter.getCount(); // Output: "Count is 0."
myCounter.delayedIncrement(); // Output: "Count is now 1."

In this example, we define a createCounter function that returns an object with four methods: increment, decrement, getCount, and delayedIncrement. The increment, decrement, and getCount methods work the same way as in the previous examples, using a count variable to keep track of the current count.

The delayedIncrement method uses an arrow function to call the increment method after a delay of 1 second, just like in the previous examples.

To create a new counter, we call the createCounter function, which returns an object with the four methods. We can then call these methods on the object to increment, decrement, get, and delay the count.

This example shows that it is possible to implement the Counter functionality without using a class, using a combination of closures and object literals. While classes provide a convenient syntax for creating objects with shared behavior, they are not the only way to achieve this in JavaScript.

Avoid JavaScript Classes when possible.

In conclusion, while classes are a powerful feature in JavaScript, they can also introduce complexities related to this and object context. In many cases, avoiding classes and instead using alternative patterns, such as the factory pattern or constructor functions, may be beneficial.

By avoiding classes, you can simplify your code and reduce the risk of errors related to this and object context. It will make your code easier to read, maintain, and debug. Of course, there are cases where classes may be the best choice for your application, such as when you need to create complex objects with shared behavior. However, weighing the benefits and drawbacks of using classes versus alternative patterns is important, and choosing the approach that best fits your needs is important.

In general, if you can achieve the same functionality without using classes, it may be worth considering alternative patterns to avoid the complexities of this and object context. Doing so lets you write cleaner, more maintainable code that is easier to work with over time.