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!