ES6 Extends 继承:现代化的类继承机制
继承是面向对象编程中的核心概念之一,它允许我们基于现有的类构建新的类,从而复用代码并扩展功能。在 ES6 之前,要在 JavaScript 中实现继承需要编写复杂的原型链代码,容易出错且难以理解。extends 关键字的出现彻底改变了这一现状,它让类之间的继承变得简单而优雅。
ES6 引入的 extends 关键字彻底改变了这一局面,它提供了一种简洁、直观的语法来实现类之间的继承关系。这让 JavaScript 的面向对象编程更加符合其他主流编程语言的习惯,大大降低了学习门槛。
Extends 继承的基本语法
extends 关键字用于创建一个类的子类,这个子类会继承父类的所有属性和方法。基本语法结构非常清晰:
// 父类(基类)
class Animal {
constructor(name) {
this.name = name;
}
speak() {
console.log(`${this.name} 发出了声音`);
}
}
// 子类(派生类)
class Dog extends Animal {
constructor(name, breed) {
super(name); // 调用父类的 constructor
this.breed = breed;
}
speak() {
console.log(`${this.name} 汪汪叫`);
}
fetch() {
console.log(`${this.name} 去捡球了`);
}
}
const myDog = new Dog("Buddy", "Golden Retriever");
myDog.speak(); // "Buddy 汪汪叫"
myDog.fetch(); // "Buddy 去捡球了"在这个例子中,Dog 类通过 extends Animal 继承了 Animal 类的所有特性。子类自动获得了父类的 speak 方法,同时还可以添加自己特有的方法如 fetch。
继承的核心机制
原型链的自动建立
当你使用 extends 时,JavaScript 引擎会自动建立正确的原型链关系:
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); // true这种自动建立的原型链关系确保了继承的正常工作。子类的原型指向父类,子类实例的原型链也正确设置。
构造函数的继承规则
在继承关系中,构造函数有一些重要的规则需要遵守:
class Vehicle {
constructor(brand) {
this.brand = brand;
console.log("Vehicle 构造函数被调用");
}
}
class Car extends Vehicle {
constructor(brand, model) {
// 必须在使用 this 之前调用 super()
super(brand);
this.model = model;
console.log("Car 构造函数被调用");
}
}
const myCar = new Car("Toyota", "Camry");
// 输出:
// "Vehicle 构造函数被调用"
// "Car 构造函数被调用"重要规则:在子类的构造函数中,必须在使用 this 关键字之前调用 super()。这是因为子类没有自己的 this 对象,而是继承父类的 this 对象。
方法重写与扩展
子类不仅可以继承父类的方法,还可以重写(override)这些方法,同时通过 super 关键字调用父类的实现:
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
introduce() {
return `我叫 ${this.name},今年 ${this.age} 岁`;
}
celebrateBirthday() {
this.age++;
console.log(`生日快乐!现在我 ${this.age} 岁了`);
}
}
class Employee extends Person {
constructor(name, age, position, salary) {
super(name, age);
this.position = position;
this.salary = salary;
}
// 重写父类方法
introduce() {
const basicIntro = super.introduce(); // 调用父类方法
return `${basicIntro},是一名 ${this.position}`;
}
// 扩展父类方法
celebrateBirthday() {
super.celebrateBirthday(); // 调用父类方法
// 添加员工特有的生日庆祝逻辑
this.salary *= 1.05; // 5% 的加薪
console.log(`生日加薪!新薪资是 $${this.salary}`);
}
// 子类特有的方法
promote(newPosition) {
this.position = newPosition;
this.salary *= 1.15; // 15% 的加薪
console.log(`恭喜升职为 ${newPosition}!新薪资 $${this.salary}`);
}
}
const employee = new Employee("Alice", 28, "前端工程师", 80000);
console.log(employee.introduce());
// "我叫 Alice,今年 28 岁,是一名 前端工程师"
employee.celebrateBirthday();
// "生日快乐!现在我 29 岁了"
// "生日加薪!新薪资是 $84000"
employee.promote("高级前端工程师");
// "恭喜升职为 高级前端工程师!新薪资 $96600"静态成员的继承
静态方法和静态属性也会被继承:
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;
// 重写静态方法
static add(a, b) {
const result = super.add(a, b);
console.log(`${a} + ${b} = ${result}`);
return result;
}
// 新增静态方法
static sphereVolume(radius) {
return (4 / 3) * this.PI * Math.pow(radius, 3);
}
}
console.log(AdvancedMath.PI); // 3.14159 (继承)
console.log(AdvancedMath.E); // 2.71828 (自有)
AdvancedMath.add(5, 3); // "5 + 3 = 8" (重写方法)
console.log(AdvancedMath.sphereVolume(5)); // 523.59833 (新增方法)私有字段的继承
ES2022 引入的私有字段(以 # 开头)也可以在继承中使用:
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.toFixed(2)} 已存入`);
}
// 子类无法直接访问父类的私有字段
// getPrivateBalance() {
// return this.#balance; // 错误:无法访问父类的私有字段
// }
}
const savings = new SavingsAccount(1000, 0.05);
savings.applyInterest(); // "利息 $50.00 已存入"
console.log(savings.getBalance()); // 1050继承的实际应用场景
1. UI 组件继承
在前端开发中,组件继承是非常常见的应用场景:
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: "按钮",
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("按钮被点击了");
}
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);
}
}
}
}
// 使用示例
const button = new Button(document.getElementById("myButton"), {
text: "点击我",
size: "large",
});
const iconButton = new IconButton(document.getElementById("myIconButton"), {
text: "删除",
icon: "🗑️",
iconPosition: "right",
});2. 数据模型继承
class Model {
constructor(data = {}) {
this.id = data.id || this.generateId();
this.createdAt = data.createdAt || new Date();
this.updatedAt = data.updatedAt || new Date();
// 批量赋值
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() {
// 模拟保存到数据库
console.log("保存数据:", this.toJSON());
return Promise.resolve(this);
}
delete() {
console.log("删除数据:", this.id);
return Promise.resolve(true);
}
toJSON() {
const { id, createdAt, updatedAt, ...data } = this;
return { id, createdAt, updatedAt, ...data };
}
static findAll() {
// 模拟从数据库查找所有记录
return Promise.resolve([]);
}
static findById(id) {
// 模拟从数据库查找特定记录
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) {
// 模拟根据邮箱查找用户
return Promise.resolve(null);
}
static authenticate(email, password) {
// 模拟用户认证
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);
}
}
// 使用示例
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
});继承的注意事项
1. 避免过深的继承层次
过深的继承层次会导致代码难以理解和维护:
// ❌ 不推荐:过深的继承层次
class Animal {}
class Mammal extends Animal {}
class Carnivore extends Mammal {}
class Feline extends Carnivore {}
class Cat extends Feline {}
class DomesticCat extends Cat {}
// ✅ 推荐:合理的继承深度
class Animal {}
class Cat extends Animal {}
class DomesticCat extends Cat {}2. 合理使用继承
只有当存在明确的 "is-a" 关系时才使用继承:
// ✅ 正确:汽车是交通工具
class Car extends Vehicle {}
// ✅ 正确:狗是动物
class Dog extends Animal {}
// ❌ 不正确:员工不是人的一部分,应该组合
class Employee extends Person {}
// ✅ 更好:组合关系
class Employee {
constructor(person, position, salary) {
this.person = person;
this.position = position;
this.salary = salary;
}
}3. 注意方法覆盖的兼容性
覆盖父类方法时,尽量保持接口的兼容性:
class Parent {
process(data) {
return data.map((item) => item.value);
}
}
class Child extends Parent {
// ✅ 保持参数和返回值类型的兼容性
process(data) {
const processed = super.process(data);
return processed.filter((item) => item > 0);
}
// ❌ 改变方法签名可能导致问题
// process(data, options) {
// // 完全不同的参数结构
// }
}继承的替代方案
虽然 extends 提供了强大的继承机制,但在某些场景下,其他模式可能更合适:
1. 组合模式
// 组合优于继承的例子
class Engine {
start() {
console.log("引擎启动");
}
stop() {
console.log("引擎停止");
}
}
class GPS {
navigate(destination) {
console.log(`导航到 ${destination}`);
}
}
class Car {
constructor() {
this.engine = new Engine();
this.gps = new GPS();
}
start() {
this.engine.start();
}
navigate(destination) {
this.gps.navigate(destination);
}
}2. Mixin 模式
const Flyable = {
fly() {
console.log(`${this.name} 正在飞行`);
},
};
const Swimmable = {
swim() {
console.log(`${this.name} 正在游泳`);
},
};
class Duck {
constructor(name) {
this.name = name;
}
}
Object.assign(Duck.prototype, Flyable, Swimmable);
const duck = new Duck("Donald");
duck.fly(); // "Donald 正在飞行"
duck.swim(); // "Donald 正在游泳"总结
ES6 的 extends 继承为 JavaScript 提供了清晰、简洁的类继承语法。通过掌握这些概念和技术,你可以:
- 建立清晰的类层次结构:使用
extends创建有意义的继承关系 - 重写和扩展方法:通过
super调用父类实现并添加新功能 - 处理复杂的继承场景:包括静态成员、私有字段等
- 选择合适的设计模式:在继承、组合和 Mixin 之间做出正确选择
继承是面向对象编程的强大工具,但要谨慎使用。合理使用继承可以让代码更加结构化和可维护,而过度使用则可能导致复杂性增加。在实际开发中,始终遵循 "is-a" 关系原则,并考虑使用组合等其他模式来实现更灵活的设计。