Skip to content

ES6 Extends 继承:现代化的类继承机制

继承是面向对象编程中的核心概念之一,它允许我们基于现有的类构建新的类,从而复用代码并扩展功能。在 ES6 之前,要在 JavaScript 中实现继承需要编写复杂的原型链代码,容易出错且难以理解。extends 关键字的出现彻底改变了这一现状,它让类之间的继承变得简单而优雅。

ES6 引入的 extends 关键字彻底改变了这一局面,它提供了一种简洁、直观的语法来实现类之间的继承关系。这让 JavaScript 的面向对象编程更加符合其他主流编程语言的习惯,大大降低了学习门槛。

Extends 继承的基本语法

extends 关键字用于创建一个类的子类,这个子类会继承父类的所有属性和方法。基本语法结构非常清晰:

javascript
// 父类(基类)
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 引擎会自动建立正确的原型链关系:

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

这种自动建立的原型链关系确保了继承的正常工作。子类的原型指向父类,子类实例的原型链也正确设置。

构造函数的继承规则

在继承关系中,构造函数有一些重要的规则需要遵守:

javascript
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 关键字调用父类的实现:

javascript
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"

静态成员的继承

静态方法和静态属性也会被继承:

javascript
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 引入的私有字段(以 # 开头)也可以在继承中使用:

javascript
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 组件继承

在前端开发中,组件继承是非常常见的应用场景:

javascript
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. 数据模型继承

javascript
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. 避免过深的继承层次

过深的继承层次会导致代码难以理解和维护:

javascript
// ❌ 不推荐:过深的继承层次
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" 关系时才使用继承:

javascript
// ✅ 正确:汽车是交通工具
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. 注意方法覆盖的兼容性

覆盖父类方法时,尽量保持接口的兼容性:

javascript
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. 组合模式

javascript
// 组合优于继承的例子
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 模式

javascript
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 提供了清晰、简洁的类继承语法。通过掌握这些概念和技术,你可以:

  1. 建立清晰的类层次结构:使用 extends 创建有意义的继承关系
  2. 重写和扩展方法:通过 super 调用父类实现并添加新功能
  3. 处理复杂的继承场景:包括静态成员、私有字段等
  4. 选择合适的设计模式:在继承、组合和 Mixin 之间做出正确选择

继承是面向对象编程的强大工具,但要谨慎使用。合理使用继承可以让代码更加结构化和可维护,而过度使用则可能导致复杂性增加。在实际开发中,始终遵循 "is-a" 关系原则,并考虑使用组合等其他模式来实现更灵活的设计。