类的哲学:面向对象与抽象

目录

第 1 节 概述

上篇我们讲了函数。函数是好东西,但数据(属性)和行为(方法)往往是绑定在一起的。

就是把相关的属性和方法打包成一个单元的工具。

JavaScript 在 ES6 引入了 class 语法。TypeScript 在此基础上做了重要增强,让它有了真正的面向对象体验。

第 2 节 类的基本写法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
class Person {
  name: string
  age: number

  constructor(name: string, age: number) {
    this.name = name
    this.age = age
  }

  greet() {
    return `你好,我是${this.name}`
  }
}

const person = new Person("张三", 18)
console.log(person.greet())  // "你好,我是张三"

属性必须提前声明——这是和 JavaScript 的主要区别。JS 中可以直接 this.name = name,TS 中要先声明类型。

第 3 节 属性修饰符

TypeScript 提供了三种访问修饰符控制属性的可见性:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class Person {
  public name: string       // 公开,默认
  protected age: number     // 受保护,自己和子类可访问
  private idCard: string    // 私有,仅自己可访问
  readonly id: number       // 只读

  constructor(name: string, age: number, idCard: string, id: number) {
    this.name = name
    this.age = age
    this.idCard = idCard
    this.id = id
  }
}

class Student extends Person {
  showInfo() {
    console.log(this.name)    // ✅ public,可访问
    console.log(this.age)     // ✅ protected,子类可访问
    console.log(this.idCard)  // ❌ private,子类不可访问
  }
}

const p = new Person("张三", 18, "123456", 1)
console.log(p.name)    // ✅ 外部可访问
console.log(p.age)     // ❌ protected,外部不可访问
console.log(p.idCard)  // ❌ private,外部不可访问
p.id = 2               // ❌ readonly,不可修改
修饰符类内部子类外部
private
protected
public
readonly独立维度,与三者组合使用

第 4 节 参数属性

TypeScript 提供了一种简写方式——在构造函数参数前加修饰符,声明和赋值一步完成:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
// 传统写法
class Person {
  private name: string
  constructor(name: string) {
    this.name = name
  }
}

// 参数属性写法 ✨
class Person {
  constructor(
    private name: string,    // 声明 + 赋值一步到位
    public readonly age: number,
    protected email?: string,
  ) {}
}

const p = new Person("张三", 18)
// 属性已经可用,代码更简洁
技巧

参数属性是 TypeScript 的特色语法。日常开发推荐多用这种写法,能省掉大量重复的属性声明代码。

第 5 节 继承

extends 实现类的继承:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class Animal {
  constructor(public name: string) {}

  move(distance: number) {
    console.log(`${this.name} 移动了 ${distance} 米`)
  }
}

class Dog extends Animal {
  constructor(name: string, public breed: string) {
    super(name)  // 必须调用 super
  }

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

  // 重写父类方法
  move(distance: number) {
    console.log(`${this.name} 跑起来了`)
    super.move(distance)  // 调用父类方法
  }
}

const dog = new Dog("旺财", "金毛")
dog.bark()     // "汪汪!"
dog.move(10)   // "旺财 跑起来了" / "旺财 移动了 10 米"
  • 子类构造函数必须调用 super()
  • 子类可以重写父类方法
  • super.xxx 调用父类方法

第 6 节 抽象类

抽象类是一种不能直接实例化的类。它定义了一组子类必须实现的方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
abstract class Animal {
  constructor(public name: string) {}

  // 普通方法 —— 子类可以直接用
  move(distance: number) {
    console.log(`${this.name} 移动了 ${distance} 米`)
  }

  // 抽象方法 —— 子类必须实现
  abstract makeSound(): void
}

// const a = new Animal("动物")  // ❌ 不能实例化抽象类

class Dog extends Animal {
  // 必须实现抽象方法
  makeSound() {
    console.log("汪汪!")
  }
}

class Cat extends Animal {
  makeSound() {
    console.log("喵喵~")
  }
}

抽象类与接口的区别:

对比抽象类接口
能否包含实现✅ 可以❌ 纯抽象
单继承一个类只能继承一个抽象类一个类可以实现多个接口
构造函数✅ 可以有
属性修饰符✅ public / protected / private

第 7 节 接口实现

类可以实现(implements)一个或多个接口,保证类满足接口约定的结构:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
interface Flyable {
  fly(): void
  land(): void
}

interface Swimmable {
  swim(): void
}

class Duck implements Flyable, Swimmable {
  fly() {
    console.log("鸭子飞起来了")
  }

  land() {
    console.log("鸭子降落了")
  }

  swim() {
    console.log("鸭子在游泳")
  }
}

接口可以约束类的实例结构,也可以约束构造函数(通过构造签名):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
interface AnimalClass {
  new (name: string): Animal  // 构造签名
}

function createAnimal(cls: AnimalClass, name: string) {
  return new cls(name)
}

class Dog {
  constructor(public name: string) {}
}

class Cat {
  constructor(public name: string) {}
}

const dog = createAnimal(Dog, "旺财")
const cat = createAnimal(Cat, "咪咪")
说明

接口 implements 是结构性检查——不要求显式声明 implements,只要结构匹配即可。但显式写出来能让代码意图更清晰。


类让你能把数据和行为组织在一起。但这里有个问题:如果你不确定某些数据类型,但又想复用同一段逻辑,该怎么办?

下一篇讲泛型——它让"不确定"变成"灵活且安全"。

目录