Super Keyword: The Bridge Connecting Subclasses and Parent Classes
When you're building a high-rise and need to continue construction on an existing foundation, you don't start from scratch. Instead, you leverage the existing foundation structure and then add new floors and features. In JavaScript class inheritance, the super keyword plays this role—it allows subclasses to call and utilize parent class functionality.
super is an important keyword introduced in ES6. It serves as both a communication bridge between subclasses and parent classes and the core mechanism for achieving code reuse and functionality extension. Mastering the use of super means you can build more elegant and efficient object-oriented programs.
Two Forms of Super Usage
The super keyword has two main usage forms: as a function call and as an object usage.
1. Super as a Function Call
When super is used as a function, it's used to call the parent class constructor:
class Animal {
constructor(name, species) {
this.name = name;
this.species = species;
console.log(`Animal constructor: created a ${species}, named ${name}`);
}
eat() {
console.log(`${this.name} is eating`);
}
}
class Dog extends Animal {
constructor(name, breed) {
// Call parent class constructor
super(name, "canine");
this.breed = breed;
console.log(`Dog constructor: breed is ${breed}`);
}
bark() {
console.log(`${this.name} barks`);
}
}
const myDog = new Dog("Buddy", "Golden Retriever");
// Output:
// "Animal constructor: created a canine, named Buddy"
// "Dog constructor: breed is Golden Retriever"
console.log(myDog);
// Dog {
// name: "Buddy",
// species: "canine",
// breed: "Golden Retriever"
// }Key Rule: In a subclass constructor, super() must be called before using the this keyword:
class WrongExample extends Animal {
constructor(name, breed) {
this.breed = breed; // ❌ Error: Using this before super()
super(name, "canine");
}
}
class CorrectExample extends Animal {
constructor(name, breed) {
super(name, "canine"); // ✅ Correct: Call super() first
this.breed = breed; // Then use this
}
}2. Super as an Object Usage
When super is used as an object, it can be used to call parent class methods:
class Vehicle {
constructor(speed) {
this.speed = speed;
}
move() {
console.log(`Vehicle moves at ${this.speed} km/h`);
}
stop() {
console.log("Vehicle stopped");
this.speed = 0;
}
getInfo() {
return `Speed: ${this.speed} km/h`;
}
}
class Car extends Vehicle {
constructor(speed, brand) {
super(speed);
this.brand = brand;
}
move() {
// Call parent method and add subclass-specific logic
console.log(`${this.brand} car started`);
super.move(); // Call parent class move method
}
getInfo() {
// Extend parent method
const baseInfo = super.getInfo(); // Call parent getInfo
return `${this.brand} Car - ${baseInfo}`;
}
stop() {
// Completely override parent method
console.log(`${this.brand} car braking`);
super.stop(); // Still call parent stop logic
console.log("Engine turned off");
}
}
const myCar = new Car(120, "Toyota");
myCar.move();
// "Toyota car started"
// "Vehicle moves at 120 km/h"
console.log(myCar.getInfo());
// "Toyota Car - Speed: 120 km/h"
myCar.stop();
// "Toyota car braking"
// "Vehicle stopped"
// "Engine turned off"Super Usage in Static Methods
super can also be used in static methods, where it calls parent class static methods:
class Database {
static connection = null;
static connect(config) {
this.connection = `Connected to database: ${config.host}`;
console.log(this.connection);
return this.connection;
}
static disconnect() {
this.connection = null;
console.log("Database connection closed");
}
static getConnectionInfo() {
return this.connection || "Not connected";
}
}
class MySQLDatabase extends Database {
static connect(config) {
console.log("Initializing MySQL connection...");
const result = super.connect(config); // Call parent static method
console.log("MySQL connection configuration complete");
return result;
}
static getConnectionInfo() {
const baseInfo = super.getConnectionInfo(); // Call parent static method
return `MySQL - ${baseInfo}`;
}
static executeQuery(sql) {
if (!this.connection) {
console.log("Please connect to database first");
return;
}
console.log(`Execute SQL: ${sql}`);
}
}
MySQLDatabase.connect({ host: "localhost", port: 3306 });
// "Initializing MySQL connection..."
// "Connected to database: localhost"
// "MySQL connection configuration complete"
console.log(MySQLDatabase.getConnectionInfo());
// "MySQL - Connected to database: localhost"
MySQLDatabase.executeQuery("SELECT * FROM users");
// "Execute SQL: SELECT * FROM users"Practical Application Scenarios of Super
1. Method Enhancement in UI Components
class UIComponent {
constructor(element, options = {}) {
this.element = element;
this.options = { ...this.getDefaultOptions(), ...options };
this.isRendered = false;
this.eventListeners = new Map();
}
getDefaultOptions() {
return {
visible: true,
disabled: false,
className: "",
styles: {},
};
}
render() {
// Basic rendering logic
this.element.style.display = this.options.visible ? "block" : "none";
this.element.disabled = this.options.disabled;
this.element.className = this.options.className;
// Apply styles
Object.assign(this.element.style, this.options.styles);
this.isRendered = true;
console.log("Basic component rendering complete");
}
bindEvents() {
// Basic event binding
if (this.options.disabled) {
this.element.style.opacity = "0.6";
this.element.style.pointerEvents = "none";
}
}
show() {
this.options.visible = true;
this.element.style.display = "block";
}
hide() {
this.options.visible = false;
this.element.style.display = "none";
}
enable() {
this.options.disabled = false;
this.element.disabled = false;
this.element.style.opacity = "1";
this.element.style.pointerEvents = "auto";
}
disable() {
this.options.disabled = true;
this.element.disabled = true;
this.element.style.opacity = "0.6";
this.element.style.pointerEvents = "none";
}
destroy() {
// Clean up event listeners
this.eventListeners.forEach((handler, event) => {
this.element.removeEventListener(event, handler);
});
this.eventListeners.clear();
console.log("Basic component destroyed");
}
}
class Modal extends UIComponent {
getDefaultOptions() {
return {
...super.getDefaultOptions(), // Call parent method to get default options
title: "Modal",
content: "",
width: "auto",
height: "auto",
closable: true,
showOverlay: true,
};
}
render() {
// Call parent basic rendering
super.render();
// Create modal structure
this.createOverlay();
this.createModalContent();
console.log("Modal rendering complete");
}
createOverlay() {
if (this.options.showOverlay) {
this.overlay = document.createElement("div");
this.overlay.className = "modal-overlay";
this.overlay.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
z-index: 1000;
`;
document.body.appendChild(this.overlay);
}
}
createModalContent() {
this.element.innerHTML = `
<div class="modal-header">
<h3>${this.options.title}</h3>
${
this.options.closable
? '<button class="modal-close">×</button>'
: ""
}
</div>
<div class="modal-body">
${this.options.content}
</div>
`;
// Set modal styles
Object.assign(this.element.style, {
position: "fixed",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
width: this.options.width,
height: this.options.height,
background: "white",
borderRadius: "8px",
boxShadow: "0 4px 20px rgba(0, 0, 0, 0.3)",
zIndex: "1001",
});
}
bindEvents() {
// Call parent basic event binding
super.bindEvents();
// Add modal-specific events
if (this.options.closable) {
const closeBtn = this.element.querySelector(".modal-close");
const closeHandler = () => this.hide();
closeBtn.addEventListener("click", closeHandler);
this.eventListeners.set("close", closeHandler);
if (this.overlay) {
const overlayHandler = () => this.hide();
this.overlay.addEventListener("click", overlayHandler);
this.eventListeners.set("overlay", overlayHandler);
}
// ESC key close
const keyHandler = (e) => {
if (e.key === "Escape") {
this.hide();
}
};
document.addEventListener("keydown", keyHandler);
this.eventListeners.set("keydown", keyHandler);
}
}
show() {
super.show(); // Call parent show method
if (this.overlay) {
this.overlay.style.display = "block";
}
// Disable background scrolling
document.body.style.overflow = "hidden";
}
hide() {
super.hide(); // Call parent hide method
if (this.overlay) {
this.overlay.style.display = "none";
}
// Restore background scrolling
document.body.style.overflow = "auto";
}
setContent(content) {
this.options.content = content;
const body = this.element.querySelector(".modal-body");
if (body) {
body.innerHTML = content;
}
}
setTitle(title) {
this.options.title = title;
const titleElement = this.element.querySelector(".modal-header h3");
if (titleElement) {
titleElement.textContent = title;
}
}
destroy() {
if (this.overlay && this.overlay.parentNode) {
this.overlay.parentNode.removeChild(this.overlay);
}
// Call parent destroy method
super.destroy();
console.log("Modal destroyed");
}
}
// Usage example
const modal = new Modal(document.createElement("div"), {
title: "Confirm Delete",
content:
"Are you sure you want to delete this item? This action cannot be undone.",
width: "400px",
});
document.body.appendChild(modal.element);
modal.render();
modal.show();2. Method Inheritance in Data Models
class BaseModel {
constructor(data = {}) {
this.id = data.id || this.generateId();
this.createdAt = data.createdAt || new Date();
this.updatedAt = data.updatedAt || new Date();
this.isDeleted = false;
this.validateAndAssign(data);
}
generateId() {
return Date.now().toString(36) + Math.random().toString(36).substr(2, 9);
}
validateAndAssign(data) {
Object.keys(data).forEach((key) => {
if (this.isValidProperty(key, data[key])) {
this[key] = data[key];
}
});
}
isValidProperty(key, value) {
// Basic validation logic
return key !== "id" && key !== "createdAt" && key !== "updatedAt";
}
update(data) {
const oldData = { ...this };
this.validateAndAssign(data);
this.updatedAt = new Date();
console.log(`Update record ${this.id}:`, {
before: oldData,
after: this.toJSON(),
});
return this.save();
}
save() {
console.log("Save data:", this.toJSON());
return Promise.resolve(this);
}
delete() {
this.isDeleted = true;
this.deletedAt = new Date();
console.log("Delete record:", this.id);
return Promise.resolve(true);
}
restore() {
this.isDeleted = false;
delete this.deletedAt;
console.log("Restore record:", this.id);
return this.save();
}
toJSON() {
const result = {
id: this.id,
createdAt: this.createdAt,
updatedAt: this.updatedAt,
};
if (this.isDeleted) {
result.deletedAt = this.deletedAt;
}
return result;
}
static find(criteria = {}) {
console.log("Find records:", criteria);
return Promise.resolve([]);
}
static findOne(criteria = {}) {
console.log("Find single record:", criteria);
return Promise.resolve(null);
}
}
class UserModel extends BaseModel {
constructor(data = {}) {
super(data);
this.role = data.role || "user";
this.loginCount = data.loginCount || 0;
this.lastLoginAt = data.lastLoginAt || null;
}
isValidProperty(key, value) {
// Call parent validation
const baseValid = super.isValidProperty(key, value);
// User model special validation
const userValidations = {
email: (val) => typeof val === "string" && val.includes("@"),
username: (val) => typeof val === "string" && val.length >= 3,
age: (val) => Number.isInteger(val) && val >= 0 && val <= 150,
role: (val) => ["user", "admin", "moderator"].includes(val),
};
return baseValid && (!userValidations[key] || userValidations[key](value));
}
login() {
this.lastLoginAt = new Date();
this.loginCount++;
this.updatedAt = new Date();
console.log(`User ${this.username} logged in, ${this.loginCount}th time`);
return this.save();
}
logout() {
console.log(`User ${this.username} logged out`);
return Promise.resolve(this);
}
changeRole(newRole) {
const oldRole = this.role;
this.role = newRole;
this.updatedAt = new Date();
console.log(
`User ${this.username} role changed from ${oldRole} to ${newRole}`
);
return this.save();
}
toJSON() {
// Call parent method to get basic data
const baseJSON = super.toJSON();
return {
...baseJSON,
username: this.username,
email: this.email,
role: this.role,
loginCount: this.loginCount,
lastLoginAt: this.lastLoginAt,
};
}
static findByEmail(email) {
console.log("Find user by email:", email);
return Promise.resolve(null);
}
static authenticate(email, password) {
console.log("User authentication:", email);
return Promise.resolve(null);
}
}
class AdminModel extends UserModel {
constructor(data = {}) {
super(data);
this.role = "admin";
this.permissions = data.permissions || [];
this.adminLevel = data.adminLevel || 1;
}
isValidProperty(key, value) {
// Call parent validation
const userValid = super.isValidProperty(key, value);
// Admin special validation
const adminValidations = {
permissions: (val) => Array.isArray(val),
adminLevel: (val) => Number.isInteger(val) && val >= 1 && val <= 5,
};
return (
userValid && (!adminValidations[key] || adminValidations[key](value))
);
}
grantPermission(permission) {
if (!this.hasPermission(permission)) {
this.permissions.push(permission);
this.updatedAt = new Date();
console.log(`Admin ${this.username} granted permission: ${permission}`);
return this.save();
}
return Promise.resolve(this);
}
revokePermission(permission) {
const index = this.permissions.indexOf(permission);
if (index > -1) {
this.permissions.splice(index, 1);
this.updatedAt = new Date();
console.log(`Admin ${this.username} revoked permission: ${permission}`);
return this.save();
}
return Promise.resolve(this);
}
hasPermission(permission) {
return this.permissions.includes(permission);
}
upgradeLevel(newLevel) {
const oldLevel = this.adminLevel;
this.adminLevel = Math.min(5, Math.max(1, newLevel));
this.updatedAt = new Date();
console.log(
`Admin ${this.username} level upgraded from ${oldLevel} to ${this.adminLevel}`
);
return this.save();
}
toJSON() {
// Call parent method
const userJSON = super.toJSON();
return {
...userJSON,
adminLevel: this.adminLevel,
permissions: [...this.permissions], // Copy array to avoid external modification
};
}
static findAdmins() {
console.log("Find all admins");
return Promise.resolve([]);
}
}
// Usage example
const admin = new AdminModel({
username: "admin",
email: "[email protected]",
permissions: ["read", "write"],
adminLevel: 3,
});
admin.grantPermission("delete").then(() => {
console.log(admin.toJSON());
});Common Pitfalls and Best Practices of Super
1. Avoid Using super in Arrow Functions
class Parent {
method() {
console.log("Parent method");
}
}
class Child extends Parent {
constructor() {
super();
// ❌ Error: super in arrow functions doesn't bind to class prototype
this.arrowMethod = () => {
super.method(); // ReferenceError: super is not defined
};
// ✅ Correct: super in regular methods works normally
this.normalMethod = function () {
// this.constructor.prototype.__proto__.method.call(this);
// But this approach is not recommended, better to use class methods
};
}
// ✅ Correct: super in class methods
instanceMethod() {
super.method(); // Works normally
}
}2. Pay Attention to this Binding in super
class Parent {
constructor() {
this.name = "Parent";
}
getName() {
return this.name;
}
}
class Child extends Parent {
constructor() {
super();
this.name = "Child";
}
getParentName() {
// this in super points to current subclass instance
return super.getName(); // Returns 'Child', not 'Parent'
}
callParentMethodDirectly() {
// Directly call parent method, but this still points to subclass instance
return Parent.prototype.getName.call(this); // Returns 'Child'
}
}
const child = new Child();
console.log(child.getParentName()); // "Child"
console.log(child.callParentMethodDirectly()); // "Child"3. Don't Access Instance Properties in Static Methods
class Parent {
constructor() {
this.instanceProperty = "instance";
}
static staticMethod() {
// ❌ Error: Cannot access instance properties in static methods
console.log(this.instanceProperty); // undefined
}
}
class Child extends Parent {
static childStaticMethod() {
// ❌ Error: super in static methods cannot access instance properties
console.log(super.instanceProperty); // undefined
}
}4. Handle Super Call Return Values Correctly
class Parent {
calculate(a, b) {
return a + b;
}
}
class Child extends Parent {
calculate(a, b, c) {
const parentResult = super.calculate(a, b); // Save parent method return value
return parentResult * c; // Calculate based on parent result
}
}
const child = new Child();
console.log(child.calculate(2, 3, 4)); // 20 ( (2+3) * 4 )Relationship Between Super and Prototype Chain
Understanding how super works helps to use it better:
class Parent {
static staticMethod() {
return "Parent static";
}
instanceMethod() {
return "Parent instance";
}
}
class Child extends Parent {
static staticMethod() {
return super.staticMethod() + " -> Child static";
}
instanceMethod() {
return super.instanceMethod() + " -> Child instance";
}
}
// Behavior of super in static methods
console.log(Child.staticMethod());
// Equivalent to: Object.getPrototypeOf(Child).staticMethod.call(this)
// Behavior of super in instance methods
const child = new Child();
console.log(child.instanceMethod());
// Equivalent to: Object.getPrototypeOf(Child.prototype).instanceMethod.call(this)
console.log(Object.getPrototypeOf(Child) === Parent); // true
console.log(Object.getPrototypeOf(Child.prototype) === Parent.prototype); // trueSummary
The super keyword is an indispensable important tool in JavaScript's class inheritance system. By mastering these concepts and techniques, you can:
- Correctly call parent class constructors: Use
super()to initialize inherited properties - Reuse parent class methods: Call and extend existing functionality through
super.method() - Handle static method inheritance: Correctly use
superin static contexts - Avoid common pitfalls: Understand the working mechanisms and limitations of
super
The use of super is not just syntactic sugar; it embodies the core ideas of "inheritance" and "extension" in object-oriented programming. Reasonable use of super can make your code more modular, maintainable, and effectively reuse existing code.
In actual development, always remember: super is the bridge connecting subclasses and parent classes. It makes inheritance relationships clearer and more powerful. Mastering the use of super means you've mastered the essence of JavaScript object-oriented programming.