JS 继承 (`extends`) 与多态:构建可扩展的类结构

嘿,大家好!今天咱们聊聊JavaScript里继承 (extends) 和多态这对好基友,看看它们怎么帮我们构建更牛掰的类结构。放心,咱们不搞那些故弄玄虚的理论,直接上代码,用大白话把这俩概念给整明白。

第一幕:继承——“爸”业我来“承”!

想象一下,你老爸是个大老板,家产万贯。继承,就跟你继承你老爸的财产一样,子类继承父类的属性和方法。这样,你不用从头开始,站在巨人的肩膀上,效率嗖嗖地!

在JS里,extends 关键字就是干这个的。

// 定义一个“老爸”类
class Animal {
  constructor(name) {
    this.name = name;
  }

  eat() {
    console.log(`${this.name} 在吃东西呢!`);
  }

  sayHello() {
    console.log("大家好,我是动物!");
  }
}

// 定义一个“儿子”类,继承 Animal
class Dog extends Animal {
  constructor(name, breed) {
    // 调用父类的 constructor
    super(name);
    this.breed = breed;
  }

  bark() {
    console.log("汪汪汪!");
  }
}

// 创建一个 Dog 的实例
const myDog = new Dog("旺财", "中华田园犬");

// 调用父类的 eat 方法
myDog.eat(); // 输出:旺财 在吃东西呢!

// 调用子类自己的 bark 方法
myDog.bark(); // 输出:汪汪汪!

// 调用父类的 sayHello 方法
myDog.sayHello(); // 输出:大家好,我是动物!

这段代码里,Animal 是父类(也被称为基类或超类),Dog 是子类(也被称为派生类)。Dog 通过 extends Animal 继承了 Animal 的所有属性和方法。

  • super() 的作用: super() 就像一个电话,告诉父类的构造函数:“喂,老爸,该你出场了!” 它负责调用父类的 constructor,确保父类的初始化逻辑也能执行。 如果子类有自己的构造函数,就必须先调用 super(),才能使用 this

继承的优势:

  • 代码重用: 子类可以直接使用父类的属性和方法,避免重复编写代码。
  • 可维护性: 如果父类有 bug,只需要修改父类的代码,所有子类都会受到影响,易于维护。
  • 可扩展性: 可以通过继承来创建新的类,扩展现有类的功能。

第二幕:多态——“变脸”大法!

多态,顾名思义,就是“多种形态”。同一个方法,在不同的对象上,可以有不同的行为。就像孙悟空的七十二变,想变啥样就变啥样。

多态主要有两种实现方式:

  1. 重写 (Override): 子类重写父类的方法,改变方法的行为。
  2. 接口 (Interface) 和抽象类 (Abstract Class) (JS里没有严格的接口,但可以用鸭子类型模拟): 定义一套规范,不同的类实现这些规范,从而实现多态。

1. 重写 (Override):

// 还是 Animal 类
class Animal {
  constructor(name) {
    this.name = name;
  }

  makeSound() {
    console.log("动物发出声音");
  }
}

// Dog 类继承 Animal
class Dog extends Animal {
  constructor(name, breed) {
    super(name);
    this.breed = breed;
  }

  // 重写父类的 makeSound 方法
  makeSound() {
    console.log("汪汪汪!");
  }
}

// Cat 类继承 Animal
class Cat extends Animal {
  constructor(name, color) {
    super(name);
    this.color = color;
  }

  // 重写父类的 makeSound 方法
  makeSound() {
    console.log("喵喵喵!");
  }
}

// 创建实例
const animal = new Animal("普通动物");
const dog = new Dog("旺财", "中华田园犬");
const cat = new Cat("咪咪", "白色");

animal.makeSound(); // 输出:动物发出声音
dog.makeSound();    // 输出:汪汪汪!
cat.makeSound();    // 输出:喵喵喵!

在这个例子里,DogCat 都继承了 AnimalmakeSound 方法,但是它们都重写了这个方法,让它们发出不同的声音。这就是多态的体现。

2. 接口 (Interface) 和抽象类 (Abstract Class) (模拟):

虽然 JavaScript 没有像 Java 或 C# 那样的接口和抽象类,但我们可以用“鸭子类型”来模拟。 鸭子类型的意思是:“如果它走起路来像鸭子,叫起来也像鸭子,那么它就是鸭子。”

// 定义一个行为规范(类似接口)
const Swimmable = {
  swim: function() {
    throw new Error("必须实现 swim 方法");
  }
};

// 实现 Swimmable 规范的 Duck 类
class Duck {
  constructor(name) {
    this.name = name;
  }

  swim() {
    console.log(`${this.name} 在游泳!`);
  }
}

// 实现 Swimmable 规范的 Fish 类
class Fish {
  constructor(name) {
    this.name = name;
  }

  swim() {
    console.log(`${this.name} 在水里游来游去!`);
  }
}

// 一个不会游泳的 Bird 类
class Bird {
  constructor(name) {
    this.name = name;
  }

  fly() {
    console.log(`${this.name} 在飞!`);
  }
}

// 检查对象是否实现了 Swimmable 规范
function isSwimmable(obj) {
  return typeof obj.swim === 'function';
}

const duck = new Duck("唐老鸭");
const fish = new Fish("小鲤鱼");
const bird = new Bird("小鸟");

if (isSwimmable(duck)) {
  duck.swim(); // 输出:唐老鸭 在游泳!
}

if (isSwimmable(fish)) {
  fish.swim(); // 输出:小鲤鱼 在水里游来游去!
}

if (isSwimmable(bird)) {
  bird.swim(); // 不会执行,因为 Bird 没有 swim 方法
} else {
    console.log(`${bird.name} 不会游泳。`); // 输出:小鸟 不会游泳。
}

在这个例子里,Swimmable 就像一个接口,定义了 swim 方法。DuckFish 都实现了这个接口,所以它们都可以游泳。Bird 没有实现这个接口,所以它不能游泳。

多态的优势:

  • 灵活性: 可以根据对象的类型来执行不同的操作,提高了代码的灵活性。
  • 可扩展性: 可以很容易地添加新的类,而不需要修改现有的代码。
  • 可维护性: 代码更加模块化,易于维护。

第三幕:继承与多态的结合——打造强大的类结构!

继承和多态通常一起使用,可以构建非常强大的类结构。

// 定义一个 Shape 类(抽象类)
class Shape {
  constructor(color) {
    this.color = color;
    if (new.target === Shape) {
      throw new Error("抽象类不能实例化!");
    }
  }

  // 定义一个抽象方法,子类必须实现
  getArea() {
    throw new Error("必须实现 getArea 方法");
  }

  draw() {
    console.log(`绘制一个 ${this.color} 的形状`);
  }
}

// 定义一个 Circle 类,继承 Shape
class Circle extends Shape {
  constructor(color, radius) {
    super(color);
    this.radius = radius;
  }

  // 实现 getArea 方法
  getArea() {
    return Math.PI * this.radius * this.radius;
  }

  draw() {
    console.log(`绘制一个 ${this.color} 的圆形,半径为 ${this.radius}`);
  }
}

// 定义一个 Rectangle 类,继承 Shape
class Rectangle extends Shape {
  constructor(color, width, height) {
    super(color);
    this.width = width;
    this.height = height;
  }

  // 实现 getArea 方法
  getArea() {
    return this.width * this.height;
  }

  draw() {
    console.log(`绘制一个 ${this.color} 的矩形,宽为 ${this.width},高为 ${this.height}`);
  }
}

// 创建实例
// const shape = new Shape("红色"); // 报错:抽象类不能实例化!
const circle = new Circle("蓝色", 5);
const rectangle = new Rectangle("绿色", 4, 6);

console.log(`圆形的面积:${circle.getArea()}`); // 输出:圆形的面积:78.53981633974483
console.log(`矩形的面积:${rectangle.getArea()}`); // 输出:矩形的面积:24

circle.draw(); // 输出:绘制一个 蓝色 的圆形,半径为 5
rectangle.draw(); // 输出:绘制一个 绿色 的矩形,宽为 4,高为 6

// 多态的体现
function printArea(shape) {
  console.log(`这个形状的面积是:${shape.getArea()}`);
}

printArea(circle); // 输出:这个形状的面积是:78.53981633974483
printArea(rectangle); // 输出:这个形状的面积是:24

在这个例子里:

  • Shape 是一个抽象类,定义了 getArea 抽象方法和 draw 普通方法。 抽象类不能被直接实例化,只能被继承。 getArea 方法强制子类必须实现,否则会报错。
  • CircleRectangle 继承了 Shape,并实现了 getArea 方法。
  • printArea 函数接受一个 Shape 类型的参数,它可以接受任何 Shape 的子类的实例,并调用它们的 getArea 方法。 这就是多态的体现。

总结:

特性 继承 (extends) 多态
目的 代码重用,构建类层次结构 灵活性,允许不同类型的对象对同一消息做出不同的响应
关键字 extends, super 重写 (Override), 接口 (Interface) (鸭子类型模拟)
优点 减少代码冗余,提高可维护性,易于扩展 提高代码的灵活性,可扩展性,可维护性
适用场景 当多个类具有相似的属性和方法时,可以使用继承。 当需要根据对象的类型来执行不同的操作时,可以使用多态。
例子 Dog extends Animal (Dog 继承 Animal 的属性和方法) AnimalDog 都有 makeSound 方法,但它们的实现不同 (重写)

注意事项:

  • 避免过度继承: 继承层次过深会使代码变得复杂难以理解和维护。
  • 优先使用组合: 在某些情况下,组合比继承更灵活。 组合是指在一个类中使用另一个类的实例作为成员变量。
  • 理解 this 的指向: 在继承和多态中,this 的指向可能会让你感到困惑。 需要仔细理解 this 的绑定规则。

总的来说,继承和多态是面向对象编程的两大基石。 掌握它们,可以让你写出更优雅、更健壮、更易于维护的代码。 希望今天的讲座对你有所帮助! 下次有机会再跟大家分享其他编程技巧!拜拜!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注