ES6 Extends Inheritance: Modern Class Inheritance Mechanism
Inheritance is one of the core concepts in object-oriented programming. It allows us to build new classes based on existing ones, thereby reusing code and extending functionality. Before ES6, implementing inheritance in JavaScript required writing complex prototype chain code that was error-prone and difficult to understand. The emergence of the extends keyword completely changed this situation, making class inheritance simple and elegant.
ES6 introduced the extends keyword, which completely changed this landscape by providing a concise, intuitive syntax for implementing inheritance relationships between classes. This makes JavaScript's object-oriented programming more aligned with the conventions of other mainstream programming languages, greatly lowering the learning barrier.
Basic Syntax of Extends Inheritance
The extends keyword is used to create a subclass of a class. This subclass will inherit all properties and methods from the parent class. The basic syntax structure is very clear:
// Parent class (base class)
class Animal {
constructor(name) {
this.name = name;
}
speak() {
console.log(`${this.name} makes a sound`);
}
}
// Child class (derived class)
class Dog extends Animal {
constructor(name, breed) {
super(name); // Call parent class constructor
this.breed = breed;
}
speak() {
console.log(`${this.name} barks`);
}
fetch() {
console.log(`${this.name} goes to fetch the ball`);
}
}
const myDog = new Dog("Buddy", "Golden Retriever");
myDog.speak(); // "Buddy barks"
myDog.fetch(); // "Buddy goes to fetch the ball"In this example, the Dog class inherits all the characteristics of the Animal class through extends Animal. The subclass automatically obtains the parent class's speak method and can also add its own specific methods like fetch.
Core Mechanisms of Inheritance
Automatic Prototype Chain Establishment
When you use extends, the JavaScript engine automatically establishes the correct prototype chain relationships:
class Animal {}
class Dog extends Animal {}
console.log(Object.getPrototypeOf(Dog) === Animal); // true
console.log(Object.getPrototypeOf(Dog.prototype) === Animal.prototype); // true
console.log(myDog instanceof Dog); // true
console.log(myDog instanceof Animal); // trueThis automatically established prototype chain relationship ensures that inheritance works properly. The subclass's prototype points to the parent class, and the subclass instance's prototype chain is also correctly set up.
Constructor Inheritance Rules
In inheritance relationships, constructors have some important rules to follow:
class Vehicle {
constructor(brand) {
this.brand = brand;
console.log("Vehicle constructor called");
}
}
class Car extends Vehicle {
constructor(brand, model) {
// Must call super() before using this
super(brand);
this.model = model;
console.log("Car constructor called");
}
}
const myCar = new Car("Toyota", "Camry");
// Output:
// "Vehicle constructor called"
// "Car constructor called"Important Rule: In a subclass constructor, you must call super() before using the this keyword. This is because the subclass doesn't have its own this object; it inherits the this object from the parent class.
Method Overriding and Extension
Subclasses can not only inherit methods from the parent class but also override (override) these methods while calling the parent class's implementation through the super keyword:
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
introduce() {
return `My name is ${this.name}, I'm ${this.age} years old`;
}
celebrateBirthday() {
this.age++;
console.log(`Happy birthday! Now I'm ${this.age} years old`);
}
}
class Employee extends Person {
constructor(name, age, position, salary) {
super(name, age);
this.position = position;
this.salary = salary;
}
// Override parent method
introduce() {
const basicIntro = super.introduce(); // Call parent method
return `${basicIntro}, and I'm a ${this.position}`;
}
// Extend parent method
celebrateBirthday() {
super.celebrateBirthday(); // Call parent method
// Add employee-specific birthday celebration logic
this.salary *= 1.05; // 5% raise
console.log(`Birthday raise! New salary is $${this.salary}`);
}
// Subclass-specific method
promote(newPosition) {
this.position = newPosition;
this.salary *= 1.15; // 15% raise
console.log(`Congratulations on promotion to ${newPosition}! New salary $${this.salary}`);
}
}
const employee = new Employee("Alice", 28, "Frontend Engineer", 80000);
console.log(employee.introduce());
// "My name is Alice, I'm 28 years old, and I'm a Frontend Engineer"
employee.celebrateBirthday();
// "Happy birthday! Now I'm 29 years old"
// "Birthday raise! New salary is $84000"
employee.promote("Senior Frontend Engineer");
// "Congratulations on promotion to Senior Frontend Engineer! New salary $96600"Inheritance of Static Members
Static methods and static properties are also inherited:
class MathHelper {
static PI = 3.14159;
static circleArea(radius) {
return this.PI * radius * radius;
}
static add(a, b) {
return a + b;
}
}
class AdvancedMath extends MathHelper {
static E = 2.71828;
// Override static method
static add(a, b) {
const result = super.add(a, b);
console.log(`${a} + ${b} = ${result}`);
return result;
}
// Add new static method
static sphereVolume(radius) {
return (4 / 3) * this.PI * Math.pow(radius, 3);
}
}
console.log(AdvancedMath.PI); // 3.14159 (inherited)
console.log(AdvancedMath.E); // 2.71828 (own)
AdvancedMath.add(5, 3); // "5 + 3 = 8" (overridden method)
console.log(AdvancedMath.sphereVolume(5)); // 523.59833 (added method)Inheritance of Private Fields
Private fields (starting with #) introduced in ES2022 can also be used in inheritance:
class BankAccount {
#balance = 0;
constructor(initialBalance) {
if (initialBalance > 0) {
this.#balance = initialBalance;
}
}
deposit(amount) {
if (amount > 0) {
this.#balance += amount;
return true;
}
return false;
}
getBalance() {
return this.#balance;
}
#validateAmount(amount) {
return amount > 0 && Number.isFinite(amount);
}
}
class SavingsAccount extends BankAccount {
#interestRate = 0.02;
constructor(initialBalance, interestRate) {
super(initialBalance);
if (interestRate > 0) {
this.#interestRate = interestRate;
}
}
applyInterest() {
const balance = this.getBalance();
const interest = balance * this.#interestRate;
this.deposit(interest);
console.log(`Interest $${interest.toFixed(2)} has been deposited`);
}
// Subclass cannot directly access parent class private fields
// getPrivateBalance() {
// return this.#balance; // Error: Cannot access parent class private fields
// }
}
const savings = new SavingsAccount(1000, 0.05);
savings.applyInterest(); // "Interest $50.00 has been deposited"
console.log(savings.getBalance()); // 1050Practical Application Scenarios of Inheritance
1. UI Component Inheritance
In frontend development, component inheritance is a very common application scenario:
class Component {
constructor(element, options = {}) {
this.element = element;
this.options = { ...this.getDefaultOptions(), ...options };
this.events = {};
this.init();
}
getDefaultOptions() {
return {
visible: true,
disabled: false,
};
}
init() {
this.render();
this.bindEvents();
}
render() {
this.element.style.display = this.options.visible ? "block" : "none";
this.element.disabled = this.options.disabled;
}
bindEvents() {}
show() {
this.options.visible = true;
this.render();
}
hide() {
this.options.visible = false;
this.render();
}
disable() {
this.options.disabled = true;
this.render();
}
enable() {
this.options.disabled = false;
this.render();
}
}
class Button extends Component {
getDefaultOptions() {
return {
...super.getDefaultOptions(),
text: "Button",
type: "button",
size: "medium",
};
}
render() {
super.render();
this.element.textContent = this.options.text;
this.element.className = `btn btn-${this.options.size}`;
this.element.type = this.options.type;
}
bindEvents() {
this.element.addEventListener("click", () => {
if (!this.options.disabled) {
this.onClick();
}
});
}
onClick() {
console.log("Button was clicked");
}
setText(text) {
this.options.text = text;
this.element.textContent = text;
}
}
class IconButton extends Button {
getDefaultOptions() {
return {
...super.getDefaultOptions(),
icon: "",
iconPosition: "left",
};
}
render() {
super.render();
const icon = this.options.icon;
if (icon) {
const iconElement = document.createElement("span");
iconElement.className = "icon";
iconElement.textContent = icon;
if (this.options.iconPosition === "left") {
this.element.insertBefore(iconElement, this.element.firstChild);
} else {
this.element.appendChild(iconElement);
}
}
}
}
// Usage examples
const button = new Button(document.getElementById("myButton"), {
text: "Click Me",
size: "large",
});
const iconButton = new IconButton(document.getElementById("myIconButton"), {
text: "Delete",
icon: "🗑️",
iconPosition: "right",
});2. Data Model Inheritance
class Model {
constructor(data = {}) {
this.id = data.id || this.generateId();
this.createdAt = data.createdAt || new Date();
this.updatedAt = data.updatedAt || new Date();
// Batch assignment
Object.assign(this, data);
}
generateId() {
return Date.now().toString(36) + Math.random().toString(36).substr(2);
}
update(data) {
Object.assign(this, data);
this.updatedAt = new Date();
return this.save();
}
save() {
// Simulate saving to database
console.log("Save data:", this.toJSON());
return Promise.resolve(this);
}
delete() {
console.log("Delete data:", this.id);
return Promise.resolve(true);
}
toJSON() {
const { id, createdAt, updatedAt, ...data } = this;
return { id, createdAt, updatedAt, ...data };
}
static findAll() {
// Simulate finding all records from database
return Promise.resolve([]);
}
static findById(id) {
// Simulate finding specific record from database
return Promise.resolve(null);
}
}
class User extends Model {
constructor(data = {}) {
super(data);
this.email = data.email;
this.username = data.username;
this.role = data.role || "user";
}
toJSON() {
const baseJSON = super.toJSON();
return {
...baseJSON,
email: this.email,
username: this.username,
role: this.role,
};
}
changeRole(newRole) {
this.role = newRole;
return this.update({ role: newRole });
}
static findByEmail(email) {
// Simulate finding user by email
return Promise.resolve(null);
}
static authenticate(email, password) {
// Simulate user authentication
return Promise.resolve(null);
}
}
class Admin extends User {
constructor(data = {}) {
super(data);
this.role = "admin";
this.permissions = data.permissions || [];
}
hasPermission(permission) {
return this.permissions.includes(permission);
}
grantPermission(permission) {
if (!this.hasPermission(permission)) {
this.permissions.push(permission);
return this.update({ permissions: this.permissions });
}
return Promise.resolve(this);
}
revokePermission(permission) {
const index = this.permissions.indexOf(permission);
if (index > -1) {
this.permissions.splice(index, 1);
return this.update({ permissions: this.permissions });
}
return Promise.resolve(this);
}
}
// Usage examples
const user = new User({
email: "[email protected]",
username: "john_doe",
});
const admin = new Admin({
email: "[email protected]",
username: "admin",
permissions: ["read", "write"],
});
admin.grantPermission("delete").then(() => {
console.log(admin.hasPermission("delete")); // true
});Considerations for Inheritance
1. Avoid Deep Inheritance Hierarchies
Overly deep inheritance hierarchies can make code difficult to understand and maintain:
// ❌ Not recommended: Overly deep inheritance hierarchy
class Animal {}
class Mammal extends Animal {}
class Carnivore extends Mammal {}
class Feline extends Carnivore {}
class Cat extends Feline {}
class DomesticCat extends Cat {}
// ✅ Recommended: Reasonable inheritance depth
class Animal {}
class Cat extends Animal {}
class DomesticCat extends Cat {}2. Use Inheritance Judiciously
Only use inheritance when there's a clear "is-a" relationship:
// ✅ Correct: A car is a vehicle
class Car extends Vehicle {}
// ✅ Correct: A dog is an animal
class Dog extends Animal {}
// ❌ Incorrect: An employee is not a person, should be composition
class Employee extends Person {}
// ✅ Better: Composition relationship
class Employee {
constructor(person, position, salary) {
this.person = person;
this.position = position;
this.salary = salary;
}
}3. Maintain Method Override Compatibility
When overriding parent class methods, try to maintain interface compatibility:
class Parent {
process(data) {
return data.map((item) => item.value);
}
}
class Child extends Parent {
// ✅ Maintain parameter and return value type compatibility
process(data) {
const processed = super.process(data);
return processed.filter((item) => item > 0);
}
// ❌ Changing method signature can cause problems
// process(data, options) {
// // Completely different parameter structure
// }
}Alternatives to Inheritance
While extends provides a powerful inheritance mechanism, other patterns may be more appropriate in certain scenarios:
1. Composition Pattern
// Composition over inheritance example
class Engine {
start() {
console.log("Engine starts");
}
stop() {
console.log("Engine stops");
}
}
class GPS {
navigate(destination) {
console.log(`Navigate to ${destination}`);
}
}
class Car {
constructor() {
this.engine = new Engine();
this.gps = new GPS();
}
start() {
this.engine.start();
}
navigate(destination) {
this.gps.navigate(destination);
}
}2. Mixin Pattern
const Flyable = {
fly() {
console.log(`${this.name} is flying`);
},
};
const Swimmable = {
swim() {
console.log(`${this.name} is swimming`);
},
};
class Duck {
constructor(name) {
this.name = name;
}
}
Object.assign(Duck.prototype, Flyable, Swimmable);
const duck = new Duck("Donald");
duck.fly(); // "Donald is flying"
duck.swim(); // "Donald is swimming"Summary
ES6's extends inheritance provides JavaScript with clear, concise class inheritance syntax. By mastering these concepts and techniques, you can:
- Establish clear class hierarchies: Use
extendsto create meaningful inheritance relationships - Override and extend methods: Use
superto call parent class implementations and add new functionality - Handle complex inheritance scenarios: Including static members, private fields, etc.
- Choose appropriate design patterns: Make the right choice between inheritance, composition, and Mixin patterns
Inheritance is a powerful tool in object-oriented programming, but use it cautiously. Proper use of inheritance can make code more structured and maintainable, while overuse can lead to increased complexity. In actual development, always follow the "is-a" relationship principle and consider using other patterns like composition to achieve more flexible designs.