当前位置: 首页 > news >正文

【阮一峰】8.类

简介

属性的类型

类的属性可以在顶层声明,也可以在构造方法内部声明。

对于顶层声明的属性,可以在声明时同时给出类型。

class Point {
  x: number;
  y: number;
}

如果不给出类型,TypeScript 会认为 x 和 y 的类型都是 any。

class Point {
  x;
  y;
}

如果声明时给出初值,可以不写类型,TypeScript 会自行推断属性的类型。

class Point {
  x = 0;
  y = 0;
}

readonly 修饰符

属性名前面加上 readonly 修饰符,就表示该属性是只读的。实例对象不能修改这个属性。

class A {
  readonly id = "foo";
}

const a = new A();
a.id = "bar"; // 报错

readonly 属性的初始值,可以写在顶层属性,也可以写在构造方法里面。

class A {
  readonly id: string;

  constructor() {
    this.id = "bar"; // 正确
  }
}

方法的类型

类的方法就是普通函数,类型声明方式与函数一致。可以使用参数默认值,以及函数重载。

class Point {
  constructor(x: number, y: string);
  constructor(s: string);
  constructor(xs: number | string, y?: string) {
    // ...
  }
}

:::tip
构造方法不能声明返回值类型,否则报错,因为它总是返回实例对象。
:::

存取器方法

存取器(accessor)是特殊的类方法,包括取值器(getter)和存值器(setter)两种方法。

TypeScript 对存取器有以下规则。

(1)如果某个属性只有 get 方法,没有 set 方法,那么该属性自动成为只读属性。

class C {
  _name = "foo";

  get name() {
    return this._name;
  }
}

const c = new C();
c.name = "bar"; // 报错

(2)get 方法与 set 方法的可访问性必须一致,要么都为公开方法,要么都为私有方法。

属性索引

类允许定义属性索引。

class MyClass {
  [s: string]: boolean | ((s: string) => boolean);

  get(s: string) {
    return this[s] as boolean;
  }
}

:::tip
由于类的方法是一种特殊属性(属性值为函数的属性),所以属性索引的类型定义也涵盖了方法。如果一个对象同时定义了属性索引和方法,那么前者必须包含后者的类型。
:::

class MyClass {
  [s: string]: boolean;
  f() {
    // 报错
    return true;
  }
}

属性存取器视同属性。

class MyClass {
  [s: string]: boolean;

  get isInstance() {
    return true;
  }
}

类的 interface 接口

implements 关键字

interface 接口或 type 别名,可以用对象的形式,为 class 指定一组检查条件。然后,类使用 implements 关键字,表示当前类满足这些外部类型条件的限制。

interface Country {
  name: string;
  capital: string;
}
// 或者
type Country = {
  name: string;
  capital: string;
};

class MyCountry implements Country {
  name = "";
  capital = "";
}

interface 只是指定检查条件,如果不满足这些条件就会报错。它并不能代替 class 自身的类型声明。

interface A {
  get(name: string): boolean;
}

class B implements A {
  get(s) {
    // s 的类型是 any,这里还需要声明s的类型string
    return true;
  }
}

类可以定义接口没有声明的方法和属性。

interface Point {
  x: number;
  y: number;
}

class MyPoint implements Point {
  x = 1;
  y = 1;
  z: number = 1;
}

implements 关键字后面,不仅可以是接口,也可以是另一个类。这时,后面的类将被当作接口。

class Car {
  id: number = 1;
  move(): void {}
}

class MyCar implements Car {
  id = 2; // 不可省略
  move(): void {} // 不可省略
}

interface 描述的是类的对外接口,也就是实例的公开属性和公开方法,不能定义私有的属性和方法。这是因为 TypeScript 设计者认为,私有属性是类的内部实现,接口作为模板,不应该涉及类的内部代码写法。

interface Foo {
  member: {}; // 报错
}

实现多个接口

类可以实现多个接口(其实是接受多重限制),每个接口之间使用逗号分隔。

class Car implements MotorVehicle, Flyable, Swimmable {
  // ...
}

但是,同时实现多个接口并不是一个好的写法,容易使得代码难以管理,可以使用两种方法替代。

第一种方法是类的继承。

class Car implements MotorVehicle {}

class SecretCar extends Car implements Flyable, Swimmable {}

类 Car 实现了接口 MotorVehicle,而 SecretCar 继承了 Car,也实现了 Flyable 和 Swimmable 接口。相当于 SecretCar 同时实现了多个接口。

第二种方法是接口的继承。

interface A {
  a: number;
}

interface B extends A {
  b: number;
}
interface MotorVehicle {
  // ...
}
interface Flyable {
  // ...
}
interface Swimmable {
  // ...
}

interface SuperCar extends MotorVehicle, Flyable, Swimmable {
  // ...
}

class SecretCar implements SuperCar {
  // ...
}

:::tip
发生多重实现时(即一个接口同时实现多个接口),不同接口不能有互相冲突的属性。
:::

类与接口的合并

TypeScript 不允许两个同名的类,但是如果一个类和一个接口同名,那么接口会被合并进类

class A {
  x: number = 1;
}

interface A {
  y: number;
}

let a = new A();
a.y = 10;

a.x; // 1
a.y; // 10

合并进类的非空属性(上例的 y),如果在赋值之前读取,会返回 undefined。

class A {
  x: number = 1;
}

interface A {
  y: number;
}

let a = new A();
a.y; // undefined

Class 类型

实例类型

TypeScript 的类本身就是一种类型,但是它代表该类的实例类型,而不是 class 的自身类型。

class Color {
  name: string;

  constructor(name: string) {
    this.name = name;
  }
}

const green: Color = new Color("green");

由于类名作为类型使用,实际上代表一个对象,因此可以把类看作对象类型的起名。事实上,TypeScript 有三种方法可以为对象类型起名:type、interface 和 class。

类的自身类型

要获得一个类的自身类型,一个简便的方法就是使用 typeof 运算符。

class Point {
  x: number;
  y: number;

  constructor(x: number, y: number) {
    this.x = x;
    this.y = y;
  }
}
function createPoint(PointClass: typeof Point, x: number, y: number): Point {
  return new PointClass(x, y);
}

类的自身类型可以写成构造函数的形式。

function createPoint(
  PointClass: new (x: number, y: number) => Point,
  x: number,
  y: number
): Point {
  return new PointClass(x, y);
}

构造函数也可以写成对象形式。

function createPoint(
  PointClass: {
    new (x: number, y: number): Point;
  },
  x: number,
  y: number
): Point {
  return new PointClass(x, y);
}

总结一下,类的自身类型就是一个构造函数,可以单独定义一个接口来表示。

结构类型原则

Class 也遵循“结构类型原则”。一个对象只要满足 Class 的实例结构,就跟该 Class 属于同一个类型。

class Foo {
  id!: number;
}

function fn(arg: Foo) {
  // ...
}

const bar = {
  id: 10,
  amount: 100,
};

fn(bar); // 正确

这种情况,运算符 instanceof 不适用于判断某个对象是否跟某个 class 属于同一类型。

如果两个类的实例结构相同,那么这两个类就是兼容的,可以用在对方的使用场合。

class Person {
  name: string;
}

class Customer {
  name: string;
}

// 正确
const cust: Customer = new Person();

总之,只要 A 类具有 B 类的结构,哪怕还有额外的属性和方法,TypeScript 也认为 A 兼容 B 的类型。

:::tip
此时无法通过 instanceof 判断某个对象是否跟某个 class 属于同一类型。
:::

空类不包含任何成员,任何其他类都可以看作与空类结构相同。因此,凡是类型为空类的地方,所有类(包括对象)都可以使用。

class Empty {}

function fn(x: Empty) {
  // ...
}

fn({});
fn(window);
fn(fn);

:::tip
确定两个类的兼容关系时,只检查实例成员,不考虑静态成员和构造方法。
:::

class Point {
  x: number;
  y: number;
  static t: number;
  constructor(x: number) {}
}

class Position {
  x: number;
  y: number;
  z: number;
  constructor(x: string) {}
}

const point: Point = new Position("");

如果类中存在私有成员(private)或保护成员(protected),那么确定兼容关系时,TypeScript 要求私有成员和保护成员来自同一个类,这意味着两个类需要存在继承关系。

// 情况一
class A {
  private name = "a";
}

class B extends A {}

const a: A = new B();

// 情况二
class A {
  protected name = "a";
}

class B extends A {
  protected name = "b";
}

const a: A = new B();

A 和 B 都有私有成员(或保护成员)name,这时只有在 B 继承 A 的情况下(class B extends A),B 才兼容 A。

类的继承

类可以使用 extends 关键字继承另一个类的所有属性和方法。

根据结构类型原则,子类也可以用于类型为基类的场合。

子类可以覆盖基类的同名方法。

class A {
  greet() {
    console.log("Hello, world!");
  }
}

class B extends A {
  greet(name?: string) {
    if (name === undefined) {
      super.greet();
    } else {
      console.log(`Hello, ${name}`);
    }
  }
}

但是,子类的同名方法不能与基类的类型定义相冲突。

class A {
  greet() {
    console.log("Hello, world!");
  }
}

class B extends A {
  // 报错
  greet(name: string) {
    console.log(`Hello, ${name}`);
  }
}

如果基类包括保护成员(protected 修饰符),子类可以将该成员的可访问性设置为公开(public 修饰符),也可以保持保护成员不变,但是不能改用私有成员(private 修饰符)。

class A {
  protected x: string = "";
  protected y: string = "";
  protected z: string = "";
}

class B extends A {
  // 正确
  public x: string = "";

  // 正确
  protected y: string = "";

  // 报错
  private z: string = "";
}

extends 关键字后面不一定是类名,可以是一个表达式,只要它的类型是构造函数就可以了。

// 例一
class MyArray extends Array<number> {}

// 例二
class MyError extends Error {}

// 例三
class A {
  greeting() {
    return "Hello from A";
  }
}
class B {
  greeting() {
    return "Hello from B";
  }
}

interface Greeter {
  greeting(): string;
}

interface GreeterConstructor {
  new (): Greeter;
}

function getGreeterBase(): GreeterConstructor {
  return Math.random() >= 0.5 ? A : B;
}

class Test extends getGreeterBase() {
  sayHello() {
    console.log(this.greeting());
  }
}

override 关键字

子类继承父类时,可以覆盖父类的同名方法。防止在继承他人的类时,会在不知不觉中就覆盖了他人的方法,TypeScript 4.3 引入了 override 关键字。

class A {
  show() {
    // ...
  }
  hide() {
    // ...
  }
}
class B extends A {
  override show() {
    // ...
  }
  override hide() {
    // ...
  }
}

可访问性修饰符

类的内部成员的外部可访问性,由三个可访问性修饰符(access modifiers)控制:publicprivateprotected

public

public 修饰符表示这是公开成员,外部可以自由访问。

class Greeter {
  public greet() {
    console.log("hi!");
  }
}

const g = new Greeter();
g.greet();

public 修饰符是默认修饰符,通常省略不写。

private

private 修饰符表示私有成员,只能用在当前类的内部,类的实例和子类都不能使用该成员。

class A {
  private x: number = 0;
}

const a = new A();
a.x; // 报错

class B extends A {
  showX() {
    console.log(this.x); // 报错
  }
}

:::tip
子类不能定义父类私有成员的同名成员。
:::

class A {
  private x = 0;
}

class B extends A {
  x = 1; // 报错
}

如果在类的内部,当前类的实例可以获取私有成员。

class A {
  private x = 10;

  f(obj: A) {
    console.log(obj.x);
  }
}

const a = new A();
a.f(a); // 10

严格地说,private 定义的私有成员,并不是真正意义的私有成员。

原因一:编译成 JavaScript 后,private 关键字就被剥离了,这时外部访问该成员就不会报错。

原因二:TypeScript 对于访问 private 成员没有严格禁止,使用方括号写法([])或者 in 运算符,实例对象就能访问该成员。

class A {
  private x = 1;
}

const a = new A();
a["x"]; // 1

if ("x" in a) {
  // 正确
  // ...
}

ES2022 引入了自己的私有成员写法#propName,解决了这些弊端。

class A {
  #x = 1;
}

const a = new A();
a["x"]; // 报错

protected

protected 修饰符表示该成员是保护成员,只能在类的内部和子类内部使用该成员,实例无法使用该成员。

class A {
  protected x = 1;
}

class B extends A {
  getX() {
    return this.x;
  }
}

const a = new A();
const b = new B();

a.x; // 报错
b.getX(); // 1

子类不仅可以拿到父类的保护成员,还可以定义同名成员。

class A {
  protected x = 1;
}

class B extends A {
  x = 2;
}

实例属性的简写形式

class Point {
  x: number;
  y: number;

  constructor(x: number, y: number) {
    this.x = x;
    this.y = y;
  }
}

等同于

class Point {
  constructor(public x: number, public y: number) {}
}

const p = new Point(10, 10);
p.x; // 10
p.y; // 10

除了 public 修饰符,构造方法的参数名只要有 privateprotectedreadonly 修饰符,都会自动声明对应修饰符的实例属性。

class A {
  constructor(
    public a: number,
    protected b: number,
    private c: number,
    readonly d: number
  ) {}
}

// 编译结果
class A {
  a;
  b;
  c;
  d;
  constructor(a, b, c, d) {
    this.a = a;
    this.b = b;
    this.c = c;
    this.d = d;
  }
}

readonly 还可以与其他三个可访问性修饰符,一起使用。

class A {
  constructor(
    public readonly x: number,
    protected readonly y: number,
    private readonly z: number
  ) {}
}

静态成员

类的内部可以使用 static 关键字,定义静态成员。

静态成员是只能通过类本身使用的成员,不能通过实例对象使用。

class MyClass {
  static x = 0;
  static printX() {
    console.log(MyClass.x);
  }
}

MyClass.x; // 0
MyClass.printX(); // 0

static 关键字前面可以使用 publicprivateprotected 修饰符。

静态私有属性也可以用 ES6 语法的#前缀表示

class MyClass {
  static #x = 0;
}

publicprotected 的静态成员可以被继承。

class A {
  public static x = 1;
  protected static y = 1;
}

class B extends A {
  static getY() {
    return B.y;
  }
}

B.x; // 1
B.getY(); // 1

泛型类

类也可以写成泛型,使用类型参数。

class Box<Type> {
  contents: Type;

  constructor(value: Type) {
    this.contents = value;
  }
}

const b: Box<string> = new Box("hello!");

:::tip
静态成员不能使用泛型的类型参数。
:::

class Box<Type> {
  static defaultContents: Type; // 报错
}

抽象类,抽象成员

在类的定义前面,加上关键字 abstract,表示该类不能被实例化,只能当作其他类的模板。

抽象类只能当作基类使用,用来在它的基础上定义子类。

abstract class A {
  id = 1;
}

class B extends A {
  amount = 100;
}

const b = new B();

b.id; // 1
b.amount; // 100

抽象类的子类也可以是抽象类,也就是说,抽象类可以继承其他抽象类。

abstract class A {
  foo: number;
}

abstract class B extends A {
  bar: string;
}

抽象类的内部可以有已经实现好的属性和方法,也可以有还未实现的属性和方法。后者就叫做“抽象成员”(abstract member),即属性名和方法名有 abstract 关键字,表示该方法需要子类实现。如果子类没有实现抽象成员,就会报错。

abstract class A {
  abstract foo: string;
  bar: string = "";
  abstract execute(): string;
}

class B extends A {
  foo = "b";
  execute() {
    return `B executed`;
  }
}

这里有几个注意点。

(1)抽象成员只能存在于抽象类,不能存在于普通类。

(2)抽象成员不能有具体实现的代码。也就是说,已经实现好的成员前面不能加 abstract 关键字。

(3)抽象成员前也不能有 private 修饰符,否则无法在子类中实现该成员。

(4)一个子类最多只能继承一个抽象类。

this 问题

类的方法经常用到 this 关键字,它表示该方法当前所在的对象。

this 参数的类型可以声明为各种对象。

function foo(this: { name: string }) {
  this.name = "Jack";
  this.name = 0; // 报错
}

foo.call({ name: 123 }); // 报错

在类的内部,this 本身也可以当作类型使用,表示当前类的实例对象。

class Box {
  contents: string = "";

  set(value: string): this {
    this.contents = value;
    return this;
  }
}

:::tip
this 类型不允许应用于静态成员。
:::

class A {
  static a: this; // 报错
}

有些方法返回一个布尔值,表示当前的 this 是否属于某种类型。这时,这些方法的返回值类型可以写成 this is Type 的形式,其中用到了 is 运算符。

class FileSystemObject {
  isFile(): this is FileRep {
    return this instanceof FileRep;
  }

  isDirectory(): this is Directory {
    return this instanceof Directory;
  }

  // ...
}

相关文章:

  • 如何写出优秀的测试用例?
  • QT基础七、用纯代码编写界面
  • 使用Keras构建图像分类模型的入门指南
  • ssm019基于ssm社区文化宣传网站+jsp(源码+包运行+LW+开题报告+任务书+技术指导)
  • 第一期——LeetCode 26删除有序数组中的重复项
  • Vulnhub中的Lupinone
  • Flutter:K线图
  • 鸿蒙(HarmonyOS)开发学习路线指南:从零到实战
  • Arduino IDE编程ESP32-C3的Flash选项
  • 轻松搭建本地大语言模型(一)Ollama安装与使用
  • 【进阶】redis篇
  • 组合模式详解(Java)
  • Linux nftables 命令使用详解
  • 基于JAVA毕业生信息招聘信息平台设计与实现
  • vllm专题(一):安装-GPU
  • 【开源免费】基于SpringBoot+Vue.JS个人博客系统(JAVA毕业设计)
  • 以下是一个使用 HTML、CSS 和 JavaScript 实现的登录弹窗效果示例
  • 什么是Spring Boot?
  • FreeRTOS-rust食用指南
  • Python程序打包 |《Python基础教程》第18章笔记
  • 习近平向第三十四届阿拉伯国家联盟首脑理事会会议致贺信
  • 专访|《内沙》导演杨弋枢:挽留终将失去的美好
  • 终于,俄罗斯和乌克兰谈上了
  • 雷军内部演讲回应质疑:在不服输、打不倒方面,没人比我们更有耐心
  • 年在沪纳税350亿人民币,这些全球头部企业表示“对上海承诺不会变”
  • 温州通报“一母亲殴打女儿致其死亡”:嫌犯已被刑拘