Learning Objectives
- Create classes with constructor and methods
- Use static methods and properties
- Implement getters and setters
- Apply inheritance with extends
- Understand private fields and methods
Basic Class Syntax
class User {
constructor(name, age) {
this.name = name;
this.age = age;
}
greet() {
return `Hello, I'm ${this.name}`;
}
}
const user = new User('John', 30);
console.log(user.greet()); // "Hello, I'm John"
console.log(user.name); // "John"
console.log(user.age); // 30
Constructor
The constructor method is called when creating a new instance:
class User {
constructor(name, email) {
this.name = name;
this.email = email;
this.createdAt = new Date();
this.posts = [];
}
}
const user = new User('John', 'john@example.com');
console.log(user.createdAt); // Current date
console.log(user.posts); // []
Methods
Define methods directly in the class:
class Calculator {
add(a, b) {
return a + b;
}
subtract(a, b) {
return a - b;
}
multiply(a, b) {
return a * b;
}
divide(a, b) {
if (b === 0) {
throw new Error('Division by zero');
}
return a / b;
}
}
const calc = new Calculator();
console.log(calc.add(5, 3)); // 8
console.log(calc.multiply(4, 5)); // 20
Static Methods
Called on the class itself, not instances:
class MathUtils {
static add(a, b) {
return a + b;
}
static max(...numbers) {
return Math.max(...numbers);
}
static random(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
}
// Call on class, not instance
console.log(MathUtils.add(5, 3)); // 8
console.log(MathUtils.max(1, 5, 3, 9)); // 9
console.log(MathUtils.random(1, 10)); // Random number 1-10
// Cannot call on instance
const utils = new MathUtils();
// utils.add(5, 3); // TypeError!
Getters and Setters
class User {
constructor(firstName, lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
get fullName() {
return `${this.firstName} ${this.lastName}`;
}
set fullName(name) {
[this.firstName, this.lastName] = name.split(' ');
}
get initials() {
return `${this.firstName[0]}${this.lastName[0]}`;
}
}
const user = new User('John', 'Doe');
console.log(user.fullName); // "John Doe" (getter)
console.log(user.initials); // "JD"
user.fullName = 'Jane Smith'; // (setter)
console.log(user.firstName); // "Jane"
console.log(user.lastName); // "Smith"
Inheritance with extends
class Animal {
constructor(name) {
this.name = name;
}
speak() {
return `${this.name} makes a sound`;
}
move() {
return `${this.name} moves`;
}
}
class Dog extends Animal {
constructor(name, breed) {
super(name); // Call parent constructor
this.breed = breed;
}
speak() {
return `${this.name} barks`;
}
fetch() {
return `${this.name} fetches the ball`;
}
}
const dog = new Dog('Rex', 'German Shepherd');
console.log(dog.speak()); // "Rex barks" (overridden)
console.log(dog.move()); // "Rex moves" (inherited)
console.log(dog.fetch()); // "Rex fetches the ball"
console.log(dog.breed); // "German Shepherd"
super Keyword
Call parent class methods:
class Animal {
constructor(name) {
this.name = name;
}
speak() {
return `${this.name} makes a sound`;
}
}
class Cat extends Animal {
speak() {
const parentSpeak = super.speak();
return `${parentSpeak} and meows`;
}
}
const cat = new Cat('Whiskers');
console.log(cat.speak());
// "Whiskers makes a sound and meows"
Private Fields
Use # for private fields (ES2022):
class BankAccount {
#balance = 0;
#pin;
constructor(initialBalance, pin) {
this.#balance = initialBalance;
this.#pin = pin;
}
deposit(amount) {
if (amount > 0) {
this.#balance += amount;
return true;
}
return false;
}
withdraw(amount, pin) {
if (pin !== this.#pin) {
throw new Error('Invalid PIN');
}
if (amount > this.#balance) {
throw new Error('Insufficient funds');
}
this.#balance -= amount;
return amount;
}
getBalance(pin) {
if (pin !== this.#pin) {
throw new Error('Invalid PIN');
}
return this.#balance;
}
}
const account = new BankAccount(1000, '1234');
account.deposit(500);
console.log(account.getBalance('1234')); // 1500
// console.log(account.#balance); // SyntaxError!
Private Methods
class User {
#password;
constructor(name, password) {
this.name = name;
this.#password = this.#hashPassword(password);
}
#hashPassword(password) {
// Private method - only accessible inside class
return btoa(password); // Simple encoding for demo
}
verifyPassword(password) {
return this.#hashPassword(password) === this.#password;
}
changePassword(oldPassword, newPassword) {
if (!this.verifyPassword(oldPassword)) {
throw new Error('Invalid password');
}
this.#password = this.#hashPassword(newPassword);
}
}
const user = new User('John', 'secret123');
console.log(user.verifyPassword('secret123')); // true
console.log(user.verifyPassword('wrong')); // false
// user.#hashPassword('test'); // SyntaxError!
Static Properties
class Config {
static apiUrl = 'https://api.example.com';
static timeout = 5000;
static version = '1.0.0';
static getConfig() {
return {
apiUrl: this.apiUrl,
timeout: this.timeout,
version: this.version
};
}
}
console.log(Config.apiUrl); // "https://api.example.com"
console.log(Config.getConfig()); // { apiUrl: ..., timeout: ..., version: ... }
Real-World Examples
User Management
class User {
constructor(name, email) {
this.name = name;
this.email = email;
this.createdAt = new Date();
this.id = User.generateId();
}
static #nextId = 1;
static generateId() {
return this.#nextId++;
}
static fromJSON(json) {
return new User(json.name, json.email);
}
toJSON() {
return {
id: this.id,
name: this.name,
email: this.email,
createdAt: this.createdAt
};
}
}
const user1 = new User('John', 'john@example.com');
const user2 = User.fromJSON({ name: 'Jane', email: 'jane@example.com' });
console.log(user1.toJSON());
Todo List
class TodoList {
#todos = [];
add(text) {
this.#todos.push({
id: Date.now(),
text,
done: false,
createdAt: new Date()
});
}
toggle(id) {
const todo = this.#todos.find(t => t.id === id);
if (todo) {
todo.done = !todo.done;
}
}
remove(id) {
this.#todos = this.#todos.filter(t => t.id !== id);
}
get completed() {
return this.#todos.filter(t => t.done);
}
get pending() {
return this.#todos.filter(t => !t.done);
}
get all() {
return [...this.#todos];
}
}
const todos = new TodoList();
todos.add('Learn JavaScript');
todos.add('Build app');
console.log(todos.pending.length); // 2
API Client
class ApiClient {
constructor(baseUrl, options = {}) {
this.baseUrl = baseUrl;
this.timeout = options.timeout || 5000;
this.headers = options.headers || {};
}
async request(endpoint, options = {}) {
const url = `${this.baseUrl}${endpoint}`;
const config = {
...options,
headers: {
'Content-Type': 'application/json',
...this.headers,
...options.headers
}
};
const response = await fetch(url, config);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
}
async get(endpoint) {
return this.request(endpoint);
}
async post(endpoint, data) {
return this.request(endpoint, {
method: 'POST',
body: JSON.stringify(data)
});
}
async put(endpoint, data) {
return this.request(endpoint, {
method: 'PUT',
body: JSON.stringify(data)
});
}
async delete(endpoint) {
return this.request(endpoint, {
method: 'DELETE'
});
}
}
const api = new ApiClient('https://api.example.com');
const users = await api.get('/users');
const newUser = await api.post('/users', { name: 'John' });
Class vs Function Constructor
Old Way (Function Constructor)
function User(name, age) {
this.name = name;
this.age = age;
}
User.prototype.greet = function() {
return `Hello, I'm ${this.name}`;
};
User.createGuest = function() {
return new User('Guest', 0);
};
const user = new User('John', 30);
Modern Way (Class)
class User {
constructor(name, age) {
this.name = name;
this.age = age;
}
greet() {
return `Hello, I'm ${this.name}`;
}
static createGuest() {
return new User('Guest', 0);
}
}
const user = new User('John', 30);
Classes are cleaner and more intuitive!
Best Practices
1. Use PascalCase for class names
class UserAccount { } // Good
class userAccount { } // Bad
2. Initialize properties in constructor
class User {
constructor(name) {
this.name = name;
this.posts = []; // Initialize
this.followers = []; // Initialize
}
}
3. Use static methods for utilities
class DateUtils {
static format(date) {
return date.toISOString();
}
static parse(str) {
return new Date(str);
}
}
4. Use private fields for encapsulation
class Counter {
#count = 0;
increment() {
this.#count++;
}
get value() {
return this.#count;
}
}
5. Call super() first in child constructor
class Dog extends Animal {
constructor(name, breed) {
super(name); // Must be first!
this.breed = breed;
}
}
Key Takeaways
- Classes provide clean OOP syntax
- Constructor initializes instances
- Methods define behavior
- Static methods/properties belong to class, not instances
- Getters/setters provide computed properties
- extends enables inheritance
- super calls parent class constructor/methods
- Private fields (#) enforce encapsulation
- Classes are syntactic sugar over prototypes
- Modern standard for OOP in JavaScript