Advanced JavaScript (ES6+)

ES6 Classes

13 min Lesson 26 of 40

ES6 Classes

ES6 introduced a new syntax for creating objects and implementing inheritance in JavaScript. Classes provide a cleaner, more intuitive syntax for constructor functions and prototypal inheritance, making object-oriented programming in JavaScript more accessible.

What are ES6 Classes?

Classes are templates for creating objects. They encapsulate data with code to work on that data. ES6 classes are syntactic sugar over JavaScript's existing prototype-based inheritance.

Key Concept: ES6 classes don't introduce a new object-oriented model to JavaScript. They're just a cleaner syntax for the prototype-based inheritance we've always had.

Class Declaration

You can declare a class using the class keyword:

// Class declaration class Person { constructor(name, age) { this.name = name; this.age = age; } greet() { return `Hello, I'm ${this.name} and I'm ${this.age} years old.`; } } // Creating instances const john = new Person("John", 30); const sarah = new Person("Sarah", 25); console.log(john.greet()); // Output: "Hello, I'm John and I'm 30 years old." console.log(sarah.greet()); // Output: "Hello, I'm Sarah and I'm 25 years old."

The Constructor Method

The constructor method is a special method for creating and initializing objects created with a class. There can only be one constructor method per class.

class Car { constructor(brand, model, year) { this.brand = brand; this.model = model; this.year = year; this.mileage = 0; // Default value } displayInfo() { return `${this.year} ${this.brand} ${this.model}`; } drive(miles) { this.mileage += miles; return `Drove ${miles} miles. Total mileage: ${this.mileage}`; } } const myCar = new Car("Toyota", "Camry", 2023); console.log(myCar.displayInfo()); // "2023 Toyota Camry" console.log(myCar.drive(100)); // "Drove 100 miles. Total mileage: 100" console.log(myCar.drive(50)); // "Drove 50 miles. Total mileage: 150"
Tip: The constructor is automatically called when you create a new instance using the new keyword. You don't need to call it manually.

Instance Methods

Methods defined inside a class are added to the prototype and shared by all instances:

class Calculator { constructor() { this.result = 0; } add(num) { this.result += num; return this; } subtract(num) { this.result -= num; return this; } multiply(num) { this.result *= num; return this; } divide(num) { if (num !== 0) { this.result /= num; } return this; } clear() { this.result = 0; return this; } getValue() { return this.result; } } const calc = new Calculator(); const result = calc.add(10).multiply(2).subtract(5).getValue(); console.log(result); // 15
Method Chaining: By returning this from methods, you can chain method calls together for more readable code.

Static Methods and Properties

Static methods and properties belong to the class itself, not to instances. They're useful for utility functions related to the class:

class MathHelper { static PI = 3.14159; static square(num) { return num * num; } static cube(num) { return num * num * num; } static circleArea(radius) { return this.PI * this.square(radius); } } // Call static methods on the class, not instances console.log(MathHelper.square(5)); // 25 console.log(MathHelper.cube(3)); // 27 console.log(MathHelper.circleArea(10)); // 314.159 console.log(MathHelper.PI); // 3.14159 // Static methods are NOT available on instances const helper = new MathHelper(); // helper.square(5); // TypeError: helper.square is not a function

Class Fields (Public and Private)

Modern JavaScript supports public and private class fields:

class BankAccount { // Public field accountType = "Savings"; // Private field (prefixed with #) #balance = 0; #pin; constructor(initialBalance, pin) { this.#balance = initialBalance; this.#pin = pin; } // Public method deposit(amount) { if (amount > 0) { this.#balance += amount; return `Deposited $${amount}. New balance: $${this.#balance}`; } return "Invalid amount"; } // Private method #validatePin(pin) { return pin === this.#pin; } withdraw(amount, pin) { if (!this.#validatePin(pin)) { return "Invalid PIN"; } if (amount > this.#balance) { return "Insufficient funds"; } this.#balance -= amount; return `Withdrew $${amount}. New balance: $${this.#balance}`; } getBalance(pin) { if (this.#validatePin(pin)) { return `Current balance: $${this.#balance}`; } return "Invalid PIN"; } } const account = new BankAccount(1000, "1234"); console.log(account.deposit(500)); // "Deposited $500. New balance: $1500" console.log(account.getBalance("1234")); // "Current balance: $1500" console.log(account.withdraw(300, "1234")); // "Withdrew $300. New balance: $1200" // Cannot access private fields // console.log(account.#balance); // SyntaxError // account.#validatePin("1234"); // SyntaxError
Important: Private fields must be declared in the class body before they can be used. They cannot be accessed or modified from outside the class.

Getters and Setters

Getters and setters allow you to control access to object properties:

class Temperature { constructor(celsius) { this._celsius = celsius; } // Getter get celsius() { return this._celsius; } // Setter set celsius(value) { if (value < -273.15) { console.log("Temperature cannot be below absolute zero"); return; } this._celsius = value; } get fahrenheit() { return (this._celsius * 9/5) + 32; } set fahrenheit(value) { this.celsius = (value - 32) * 5/9; } get kelvin() { return this._celsius + 273.15; } set kelvin(value) { this.celsius = value - 273.15; } } const temp = new Temperature(25); console.log(temp.celsius); // 25 console.log(temp.fahrenheit); // 77 console.log(temp.kelvin); // 298.15 temp.fahrenheit = 86; console.log(temp.celsius); // 30 temp.kelvin = 300; console.log(temp.celsius); // 26.85

Class Expressions

Like functions, classes can also be defined using expressions:

// Named class expression const Rectangle = class Rect { constructor(width, height) { this.width = width; this.height = height; } area() { return this.width * this.height; } }; // Anonymous class expression const Circle = class { constructor(radius) { this.radius = radius; } area() { return Math.PI * this.radius * this.radius; } }; const rect = new Rectangle(10, 5); console.log(rect.area()); // 50 const circle = new Circle(7); console.log(circle.area()); // 153.938...

Classes vs Constructor Functions

Here's how ES6 classes compare to the traditional constructor function approach:

// Old way: Constructor function function PersonOld(name, age) { this.name = name; this.age = age; } PersonOld.prototype.greet = function() { return `Hello, I'm ${this.name}`; }; // New way: ES6 class class PersonNew { constructor(name, age) { this.name = name; this.age = age; } greet() { return `Hello, I'm ${this.name}`; } } // Both create functionally equivalent objects const person1 = new PersonOld("John", 30); const person2 = new PersonNew("Jane", 25); console.log(person1.greet()); // "Hello, I'm John" console.log(person2.greet()); // "Hello, I'm Jane"
Best Practice: Use ES6 classes for new code. They're cleaner, easier to read, and align with modern JavaScript standards. However, understanding constructor functions is still important for reading legacy code.

Practice Exercise:

Challenge: Create a Book class with the following requirements:

  • Properties: title, author, pages, currentPage (starts at 0)
  • Method read(pages): advances currentPage
  • Method getProgress(): returns percentage read
  • Getter isFinished: returns true if book is completed
  • Static method compare(book1, book2): returns which book has more pages

Solution:

class Book { constructor(title, author, pages) { this.title = title; this.author = author; this.pages = pages; this.currentPage = 0; } read(pages) { this.currentPage = Math.min(this.currentPage + pages, this.pages); return `Reading... Currently on page ${this.currentPage}`; } getProgress() { const percentage = (this.currentPage / this.pages * 100).toFixed(1); return `${percentage}% complete`; } get isFinished() { return this.currentPage >= this.pages; } static compare(book1, book2) { if (book1.pages > book2.pages) { return `"${book1.title}" is longer`; } else if (book2.pages > book1.pages) { return `"${book2.title}" is longer`; } return "Both books have the same number of pages"; } } const book = new Book("JavaScript: The Good Parts", "Douglas Crockford", 176); console.log(book.read(50)); // "Reading... Currently on page 50" console.log(book.getProgress()); // "28.4% complete" console.log(book.isFinished); // false const book2 = new Book("Eloquent JavaScript", "Marijn Haverbeke", 472); console.log(Book.compare(book, book2)); // "Eloquent JavaScript" is longer

Summary

In this lesson, you learned:

  • ES6 classes provide cleaner syntax for object-oriented programming
  • The constructor method initializes new instances
  • Instance methods are shared by all instances via the prototype
  • Static methods belong to the class itself, not instances
  • Private fields (#) provide true encapsulation
  • Getters and setters control property access
  • Classes are syntactic sugar over prototype-based inheritance
Next Up: In the next lesson, we'll explore class inheritance and how to create class hierarchies using the extends keyword!