JavaScript Classes
JavaScript classes are templates for creating objects. They encapsulate data and behavior. Classes were introduced in ES6.
Class Basics
Classes in JavaScript are templates for creating objects with shared structure and behavior. Introduced in ES6 (ES2015), classes provide a cleaner, more intuitive syntax for object-oriented programming compared to the older prototype-based approach. While classes are syntactic sugar over JavaScript's existing prototype system, they make code more readable and easier to understand, especially for developers coming from class-based languages like Java or C++.
The constructor() method is a special method that's automatically called when you create a new instance of the class using the new keyword. The constructor initializes the object's properties and sets up any initial state. Each class can have only one constructor method—if you define multiple constructors, JavaScript will throw an error. Inside the constructor, this refers to the newly created instance.
Methods in a class are defined without the function keyword, making the syntax cleaner and more concise. Class methods are automatically added to the class's prototype, so all instances share the same method implementations rather than each instance having its own copy. This is memory-efficient and follows the standard JavaScript prototype inheritance model.
The new keyword is required to create an instance from a class. Calling a class without new will throw a TypeError. When you use new, JavaScript creates a new empty object, sets its prototype to the class's prototype, calls the constructor with this bound to the new object, and returns the object. This is different from regular functions, which can be called with or without new.
Unlike function declarations, class declarations are not hoisted. You must define a class before you can use it in your code. Attempting to instantiate a class before its declaration will result in a ReferenceError. This is similar to let and const behavior and helps prevent bugs from using classes before they're fully defined. Classes can also be defined as expressions: const MyClass = class { } allows you to assign a class to a variable.
// Class declaration
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
greet() {
return `Hello, I'm ${this.name}`;
}
haveBirthday() {
this.age++;
}
}
// Create instance
const john = new Person("John", 30);
console.log(john.greet()); // "Hello, I'm John"
john.haveBirthday();
console.log(john.age); // 31
// Class expression
const Animal = class {
constructor(type) {
this.type = type;
}
};
Class Inheritance
The extends keyword creates a subclass (child class) that inherits from a parent class (superclass). Inheritance allows you to create specialized versions of existing classes, reusing and extending their functionality without duplicating code. The child class automatically inherits all properties and methods from the parent class, and can add its own unique properties and methods or override inherited ones.
When a child class has a constructor, you must call super() before accessing this. The super() function calls the parent class's constructor, properly initializing the inherited properties. Forgetting to call super() in a child class constructor will cause a ReferenceError when you try to use this. You pass arguments to super() that the parent constructor expects, allowing the parent to set up its portion of the object's state.
The super keyword can also be used to call parent class methods from within child class methods: super.methodName(). This is useful when you want to override a method but still include the parent's implementation. For example, a child class's speak() method might call super.speak() to include the parent's behavior, then add additional functionality. This allows you to extend rather than completely replace parent behavior.
Child classes inherit all properties and methods from the parent class through the prototype chain. If you call a method on a child instance and that method doesn't exist on the child class, JavaScript looks up the prototype chain to find it in the parent class. This inheritance chain can extend multiple levels deep—a child class can extend another child class, creating a grandparent-parent-child relationship.
You can override parent methods by defining a method with the same name in the child class. When you call that method on a child instance, JavaScript uses the child's version instead of the parent's (method overriding). This polymorphism allows child classes to provide specialized implementations while maintaining the same interface. You can completely replace the parent's implementation or augment it by calling super.methodName() within the overriding method.
// Parent class
class Animal {
constructor(name) {
this.name = name;
}
speak() {
return `${this.name} makes a sound`;
}
}
// Child class
class Dog extends Animal {
constructor(name, breed) {
super(name); // Call parent constructor
this.breed = breed;
}
speak() {
return `${this.name} barks`;
}
wagTail() {
return `${this.name} wags tail`;
}
}
const dog = new Dog("Rex", "Labrador");
console.log(dog.speak()); // "Rex barks"
console.log(dog.wagTail()); // "Rex wags tail"
Static Methods and Getters/Setters
Static methods are defined with the static keyword and belong to the class itself rather than to instances of the class. You call static methods directly on the class name (ClassName.methodName()), not on instances. Static methods are commonly used for utility functions related to the class, factory methods that create instances, or operations that work with multiple instances (like comparing two objects). Instances cannot call static methods—trying to do so results in a TypeError.
Static methods don't have access to instance properties or this in the way instance methods do. Since they're called on the class itself, this inside a static method refers to the class, not an instance. Static methods are useful for creating helper functions that are related to the class conceptually but don't need access to instance-specific data. For example, Math.max() and Array.from() are static methods on their respective built-in classes.
Getters are special methods defined with the get keyword that allow you to access a property using dot notation while actually executing a function. Getters look like properties when you use them (obj.propertyName instead of obj.propertyName()), but they run code when accessed. This is useful for computed properties that are calculated based on other properties, for formatting data before returning it, or for providing a clean API where internal implementation details are hidden.
Setters are defined with the set keyword and allow you to run code when a property is assigned a value. Like getters, setters use property syntax (obj.propertyName = value) but execute a function. Setters are perfect for validation—you can check if a value is valid before setting a property, throw errors for invalid inputs, or automatically convert/normalize the input. This provides controlled access to object properties instead of allowing direct manipulation.
Getters and setters work together to provide property-like access with function-like control. A common pattern is to use a private property (often prefixed with an underscore like _radius) internally and provide public getter/setter access to it. This encapsulation allows you to change the internal implementation without breaking the external API. Computed properties can be getters without setters (read-only properties), while properties that need validation typically have both getter and setter.
class Circle {
constructor(radius) {
this._radius = radius;
}
// Getter
get radius() {
return this._radius;
}
// Setter
set radius(value) {
if (value > 0) {
this._radius = value;
}
}
get area() {
return Math.PI * this._radius ** 2;
}
// Static method
static compare(c1, c2) {
return c1.radius - c2.radius;
}
}
const circle = new Circle(5);
console.log(circle.area); // 78.54
circle.radius = 10;
console.log(circle.area); // 314.16
// Call static method
const c1 = new Circle(5);
const c2 = new Circle(10);
console.log(Circle.compare(c1, c2)); // -5