Hello, Let‘s Take a Deep Dive into JavaScript Classes

Classes seem to stir up some controversy in the JavaScript world. When they landed in ES6, many developers debated whether classes have a place in a prototypal language like JavaScript. Others welcomed the more familiar syntax.

But love them or hate them, classes are now a integral part of the language. As a JavaScript developer, having a solid grasp of classes will serve you well.

So in this comprehensive guide, we are going to peel back the layers on JavaScript classes and uncover their full potential. By the end, you should feel comfortable using classes in your next application!

A Brief History of Classes in JavaScript

JavaScript has always had a prototypal inheritance model rather than being truly class-based like Java or C#. If you aren‘t familiar, this means objects inherit directly from other objects rather than abstract classes.

But this proved challenging for developers used to class-oriented languages. There were often requests for more ergonomic syntax for creating prototype-based hierarchies.

Library authors came up with solutions – Backbone introduced a extend method while CoffeeScript offered class keywords. These took off in popularity.

Seeing the demand, TC39 finally added class syntax sugar in ECMAScript 2015. This introduced the class keyword along with other class semantics.

Under the hood classes still use the prototypal model. The class syntax is just wrapper on top of prototypes. But this implementation detail is mostly hidden from developers.

The Great Classes Debate

Not everyone was thrilled about adding classes to JavaScript. After all, many felt prototypes were a defining characteristic of the language.

Critics argued classes don‘t truly fit into JavaScript‘s flexible prototypal model. Features like inheritance can be unintuitive compared to simple object composition.

However, the class syntax has gained widespread adoption among developers. Classes lower the barriers to entry for folks coming from classical OO languages. And give cleaner way to create prototype chains.

These days, most view debate around classes as largely academic. Classes are here to stay as a convenient way to build reusable components.

Let‘s look at some key benefits of using classes:

Familiar Syntax – Much easier for translating OOP experience from other languages. Reads closer to Java/C#/Python code.

Encapsulation – Group related properties/methods together while hiding implementation details. Prevent outside tampering.

Reusability – Define common logic in parent classes that subclasses can inherit. Child classes only specify unique behavior.

Organization – Logical way to structure code with single responsibility classes. Improved cohesion within class methods.

Clearly classes add nice ergonomics on top of prototypal model while encouraging modular code.

With that quick history lesson out of the way, let‘s dive deeper into how classes work!

Browser Compatibility

Classes are supported in all modern browsers but you may need to transpile code for legacy browser support.

Tools like Babel can convert class syntax to constructor functions that work everywhere. This involved behind the scenes but as developer you can write code using new syntax worry-free!

Alright, on to the good stuff!

Declaring Classes

The basic syntax for declaring a class is straightforward – we use the class keyword followed by the name of our class.

For example here is simple User class:

class User {
  // Class body...  
}

The body of the class is enclosed in curly braces {}. This contains all methods and properties that should belong to the class.

Later we instantiate instances of our class using the new keyword:

const user1 = new User();
const user2 = new User(); 

This invokes the constructor method to create brand new instances of User.

Now let‘s explore some important elements that live inside the class body.

Constructor Method

Constructor is a special method that initializes class instances. When we call new User(), the constructor runs and sets up the object:

class User {

  constructor(name) {
    this.name = name;
  }

}

const me = new User(‘Sam‘); // Runs constructor

Notice a few things:

  • Constructor is called constructor() by convention but isn‘t a reserved keyword
  • Inside constructor use this to set properties belong to instance
  • We can pass arguments like name to parametrize each instance

So in general constructor is used to setup instance state, perhaps based on arguments.

Instance Methods

We can also add instance methods inside the body.

These belong to created object from the class blueprint and can access this and any properties.

class User {

  name = ‘default‘;

  printName() {
    console.log(this.name); 
  }

}

const me = new User();
me.printName();

Simple enough! Now every User instance inherits this printName method.

Getters & Setters

Getters and setters control access to properties. They run custom logic when assigning or accessing a property.

Why useful? We can:

  • Validate values before allowing change
  • Format data when accessed
  • Calculate properties dynamically

Here is example with getter and setter for name:

class User {

  _name = ‘default‘;

  get name() {
    return this._name.toUpperCase();
  }

  set name(newName) {
    if (!newName) throw Error(‘Invalid‘);
    this._name = newName;
  }

}

const me = new User();
console.log(me.name); // DEFAULT

me.name = ‘sam‘; 

Now assigning name goes through setter while accessing goes through getter. Powerful!

Inheritance

We can leverage inheritance to extend classes. This allows a child class to reuse logic from parent class.

The extends keyword establishes an inheritance chain:

class Animal {
  walk() {
    console.log(‘walking...‘);
  }
}

class Bird extends Animal {
  fly() {
    console.log(‘flying...‘); 
  } 
}

const eagle = new Bird();
eagle.fly(); // flying...
eagle.walk(); // walking...

Any methods on Animal become available on objects created from Bird. Properties are inherited as well.

The child can also override parent methods like walk() to customize those.

Inheritance enables very helpful code reuse across hierarchies!

Now those are basics – but classes have even more cool features…

Advanced Class Tricks

We‘ve covered constructors, methods, getters, setters. Now I want to demonstrate some more advanced capabilities.

Such as private properties, static members, mixins, and more!

Private Properties

We can create private properties only accessible within the class itself. Not exposed on instances at all.

Syntax is simple – prefix property with # hash symbol:

class Counter {

  #count = 0;

  increment() {
    this.#count++; 
  }

}

const myCounter = new Counter();
myCounter.#count; // Error!

These #privateFields can be accessed via this but not directly on instance. Great for encapsulation!

Static Members

Static properties and methods belong directly on the class constructor itself:

class MyMath {

  static PI = 3.14;  

  static circleArea(r) {
    return this.PI * r ** 2;
  }

}

MyMath.PI; // 3.14
MyMath.circleArea(3); // 28.26

No need instantiate to access them – call directly on class! Useful for utilities.

Mixins

Mixins allow classes to inherit from multiple sources, similar to multiple inheritance:

function FlyingMixin(BaseClass) {
  return class extends BaseClass {
    fly() {
      console.log(‘Flying!‘);
    }
  }
}

class Animal {}

class Bird = FlyingMixin(Animal);

const eagle = new Bird();
eagle.fly();

Here FlyingMixin takes base class to extend, adds extra methods like fly(), returns subclass. Flexible!

Iterators

We can make classes iterable by adding a Symbol.iterator method:

class RandomNumbers {

  [Symbol.iterator]() {
    return {
      next() {
        return { value: Math.random() };
      }
    }
  }

}

const rand = new RandomNumbers();
for (let num of rand) { 
  console.log(num); // print random values
} 

Now we can loop over instances using for…of loops!

Class Design Patterns

Classes really shine when applied to design patterns like factories, singletons, observers, and more!

Factory Functions

Factory function instantiate right subclass without exposing logic:

class Dog {
  bark() {} 
}

class Cat {
  meow() {}
}

function createPet(type) {
  if (type === ‘dog‘) return new Dog();
  if (type === ‘cat‘) return new Cat();
}

const myPet = createPet(‘cat‘); // Creates Cat instance
myPet.meow();

Keeps new call abstracted behind factory! Extendable.

Singleton Classes

Ensure only one instance exists across codebase using static property:

class Config {

  static instance;

  constructor() {
    if (Config.instance) {
      return Config.instance;
    }

    Config.instance = this; 
  }

}

const config = new Config();
const config2 = new Config();

config === config2; // true - same instance!

Now always guaranteed single instance created maximum.

Observer Pattern

Trigger callbacks when event occurs using class events system:

class User {

  observers = [];

  subscribe(fn) {
    this.observers.push(fn); 
  }

  broadcast(data) {
    this.observers.forEach(fn => fn(data));
  }

}

const user = new User();

user.subscribe(data => {
  console.log(‘Broadcast:‘, data);  
});

user.broadcast(‘Hello!‘); // Logs "Broadcast: Hello!"

Flexible pub/sub approach!

Decorators

Wrap and modify class behavior:

class MacBook {
  cost() { 
    return 997; 
  }
}

function memoryUpgrade(cls) {
  return class extends cls {
    cost() {
      return super.cost() + 75;
    } 
  }
}

const UpgradedMacBook = memoryUpgrade(MacBook);

new UpgradedMacBook().cost(); // 1072

Leverage inheritance to decorate classes!

Reactive Classes

Classes can integrate nicely with reactive programming paradigms like RxJS Observables:

import { Observable } from ‘rxjs‘;

class NewsFeed {

  observable = new Observable(); 

  startStreaming() {
    setInterval(() => {
      this.observable.next(getLatestNews()); 
    }, 3000);
  } 

}

const newsFeed = new NewsFeed();
newsFeed.startStreaming();

newsFeed.observable.subscribe(newsItem => {
  console.log(newsItem); 
});

Now we can reactively stream latest news updates to subscribers!

The RxJS library takes this to next level with Subjects and more.

Strict Typing with TypeScript

TypeScript takes classes to strictly typed level. Some nice additions:

class User {

  // Fields are typed
  name: string;

  // Access modifiers
  private id: number;

  // Initialize properties  
  constructor(name: string) {
    this.name = name;
  }

  // Method arguments typed
  print(msg: string) {
    console.log(`${this.name} -> ${msg}`);
  }

}

const me = new User(‘Sam‘);

Type safety helps catch bugs during development! Recommended for larger codebases.

Classes in Frontend Frameworks

Classes are integral part React, Vue, Angular and more component-based architectures:

class Counter extends React.Component {

  state = {
    count: 0
  }

  increment() { 
    this.setState(prevState => ({
      count: prevState.count + 1  
    }));
  }

  render() {
    return (
      <div>{this.state.count}</div>
      <button onClick={this.increment}>Increment</button>  
    );
  }

} 

Great way to encapsulate component logic + markup together!

And there we have it – a tour through what JavaScript classes have to offer.

We covered the basics like constructors and methods, as well as advanced topics like mixins and integration with types, reactive programming, and frontend frameworks.

I hope you feel empowered to start building reusable components with classes for your next application! Let me know if you have any other questions.

Happy coding!