第 8 章 类

Flying
2024-03-10 / 0 评论 / 36 阅读 / 正在检测是否收录...

一些函数式开发人员
尽量不要使用类
对我来说太激进了

2010年初,TypeScript创建和发布时,JavaScript 的世界与今天有很大的不同。像箭头函数和 let/const 变量这些后来会在 ES2015 中标准化的功能仍然是遥不可及的希望。Babel 离它的第一次提交还有几年的时间;它的前身工具,如将新 JavaScript 语法转换为旧语法的 Traceur,还没有完全被主流采用。

TypeScript 的早期营销和功能集就是为这个世界量身定制的。除了它的类型检查,还强调了它的转换器 —— 类是一个常见的例子。如今,TypeScript 的类支持只是支持所有 JavaScript 语言功能的众多功能之一。TypeScript 既不鼓励也不劝阻使用类或任何其他流行的 JavaScript 模式。

类方法

TypeScript 通常以理解独立函数相同的方式理解方法。除非给出类型或默认值,否则参数类型默认为 any;调用该方法需要可接受的参数数量;如果函数不是递归的,通常可以推断返回类型。

此代码片段使用 greet 类方法定义了一个 Greeter 类,该方法接受单个 number 类型的必填参数:

class Greeter {
  greet(name: string) {
    console.log(`${name}, do your stuff!`);
  }
}

new Greeter().greet("Miss Frizzle"); // Ok

new Greeter().greet();
//    ~~~~~
// 错误:应有 1 个参数,但获得 0 个。

类构造函数在参数方面与典型的类方法相同。TypeScript 将执行类型检查,以确保提供了正确数量的具有正确类型的参数调用方法。

这个 greeting 构造函数还需要提供 message: string 参数:

class Greeted {
  constructor(message: string) {
    console.log(`As I always say: ${message}!`);
  }
}

new Greeted("take chances, make mistakes, get messy");

new Greeted();
// Error: Expected 1 arguments, but got 0.

我将在本章后面介绍子类上下文中的构造函数。

类属性

若要在 TypeScript 中读取或写入类的属性,必须在类中显式声明该属性。类属性使用与接口相同的语法声明:它们的名称后跟类型注解(可选)。

TypeScript 不会尝试从构造函数中的赋值推断类上可能存在哪些成员。

在此示例中,允许将 destination 分配给 FieldTrip 类的实例并在该实例上访问,因为它显式声明为 string。不允许在构造函数中使用 this.nonexistent 赋值,因为该类未声明 nonexistent 属性:

class FieldTrip {
  destination: string;

  constructor(destination: string) {
    this.destination = destination; // Ok
    console.log(`We're going to ${this.destination}!`);

    this.nonexistent = destination;
    //    ~~~~~~~~~~~
    // 错误:类型“FieldTrip”上不存在属性“nonexistent”。
  }
}

显式声明类属性允许 TypeScript 快速了解类实例上允许或不允许存在的内容。稍后,当使用类实例时,如果代码尝试访问未知存在的类实例的成员,例如使用此延续的 trip.nonexistent,则 TypeScript会使用这种理解来给出类型错误:

const trip = new FieldTrip("planetarium"); 

trip.destination; // Ok

trip.nonexistent;
//    ~~~~~~~~~~~
// 错误:类型“FieldTrip”上不存在属性“nonexistent”。

函数属性

让我们回顾一下 JavaScript 的方法作用域和语法基础,因为如果你不习惯它们,它们可能会让人感到惊讶。JavaScript 包含两种语法,用于将类上的成员声明为可调用函数函数:方法属性

我已经展示了在成员名称后加上括号的方法,例如 myFunction(){}。该方法将函数分配给类原型,因此所有类实例都使用相同的函数定义。

下面这个 WithMethod 类声明了一个 myMethod 方法,所有实例都可以引用它:

class WithMethod {
  myMethod() {}
}

new WithMethod().myMethod === new WithMethod().myMethod; // true

另一种语法是声明一个值为函数的属性。这会为每个类的实例创建一个新函数,这对于 ()=> 箭头函数很有用,其 this 作用域应始终指向类实例(以创建每个类实例的新函数的时间和内存成本)。

这个 WithProperty 类包含一个名称为 myProperty 的属性, 类型为 () => void,每个类实例重新创建该属性:

class WithProperty {
  myProperty: () => {}
}
// 译者注:原文 new WithMethod().myProperty === new WithMethod().myProperty; // false
new WithMethod().myProperty === new WithMethod().myProperty; // false

可以使用与类方法和标准函数相同的语法为函数属性提供参数和返回类型。毕竟,它们是分配给类成员的值,而该值恰好是一个函数。

这个 WithPropertyParameters 类具有 takesParameters 属性,其类型为 (input: string) => number

class WithPropertyParameters {
    takesParameters = (input: boolean) => 
    input ? "Yes" : "No";
}

const instance = new WithPropertyParameters(); 

instance.takesParameters(true); // Ok 

instance.takesParameters(123);
//    ~~~
// 错误:类型“number”的参数不能赋给类型“boolean”的参数。

初始化检查

启用严格的编译器设置时,TypeScript会检查每个在构造函数中没有包括 undefined 类型的属性是否在构造函数中被赋值。这种严格的初始化检查非常有用,因为它可以防止代码意外地忘记为类属性赋值。

以下 WithValue 类没有为其 unused 属性赋值,TypeScript 将其识别为类型错误:

class WithValue {
  immediate = 0; // Ok
  later: number; // Ok (set in the constructor)
  mayBeUndefined: number | undefined; // Ok (allowed to be undefined)

  unused: number;
  // 错误:属性“unused”没有初始化表达式,且未在构造函数中明确赋值。

  constructor() {
    this.later = 1;
  }
}

如果没有严格的初始化检查,即使类型系统表明不能访问,类实例也可能被允许访问一个可能是 undefined 的值。

如果没有进行严格的初始化检查,此示例将愉快地编译,但生成的 JavaScript 在运行时会崩溃。

class MissingInitializer {
  // 属性“property”没有初始化表达式,且未在构造函数中明确赋值。
  property: string;
}

new MissingInitializer().property.length;

十亿美元的错误再次发生!

使用 TypeScript 的 strictPropertyInitialization 编译选项配置严格的属性初始化检查在第 12 章 “使用 IDE 功能”中介绍。

明确分配的属性

尽管严格的初始化检查在大多数情况下都很有用,但你可能会遇到一些情况,即在类构造函数之后有意取消分配类属性。如果绝对确定某个属性不该应用严格的初始化检查,则可以在其名称后添加 ! 以禁用该检查。这样做是在 TypeScript 中断言属性在第一次使用之前将被分配一个非 undefined 的值。

这个 ActivitiesQueue 类应该独立于其构造函数重新初始化任意次数,因此它的 pending 属性必须使用 ! 断言:

class ActivitiesQueue {
  pending!: string[]; // Ok

  initialize(pending: string[]) {
    this.pending = pending;
  }

  next() {
    return this.pending.pop();
  }
}

const activities = new ActivitiesQueue();

activities.initialize(['eat', 'sleep', 'learn'])
activities.next();
需要对类属性禁用严格的初始化检查通常是代码设置方式不适合类型检查的标志。与其在属性上添加!断言并降低类型安全性,不如考虑重构该类,以不再需要断言。

可选属性

就像接口一样,TypeScript 中的类可以通过在其声明名称后添加 ? 来将属性声明为可选的。可选属性的行为与类型恰好为包含 | undefined 的联合类型的属性大致相同。严格的初始化检查不会介意它们是否未在其构造函数中显式设置。

下面的 OptionalProperty 类将其 property 标记为可选,因此在类的构造函数中可以不对其进行赋值,而不会触发严格的属性初始化检查:

class MissingInitializer {
  property?: string;
}

new MissingInitializer().property?.length; // Ok

new MissingInitializer().property.length;
// 错误:对象可能为“未定义”。

只读属性

同样,与接口非常相似,TypeScript 中的类可以通过在其声明名称前添加 readonly 关键字来将属性声明为只读。readonly 关键字纯粹存在于类型系统中,在编译为 JavaScript 时被删除。

声明为 readonly 的属性只能在声明它们的位置或在构造函数中分配初始值。任何其他位置(包括类本身的方法)只能从属性中读取,而不能写入属性。

在此示例中,Quote 类上的 text 属性在构造函数中被分配一个值,但其他用法会导致类型错误:

class Quote {
  readonly text: string;

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

  emphasize() {
    this.text += "!";
    //    ~~~~
    // 错误:无法为“text”赋值,因为它是只读属性。
  }
}

const quote = new Quote(
  "There is a brilliant child locked inside every student."
);

Quote.text = "Ha!";
// 错误:类型“typeof Quote”上不存在属性“text”。
代码的外部用户(例如你发布的任何 npm 包的使用者)可能不尊重只读修饰符,尤其是在他们编写 JavaScript 并且没有类型检查的情况下。如果需要真正的只读保护,请考虑使用 # 私有字段和/或 get() 函数属性。

使用初始值为原始值的 readonly 声明的属性与其他属性有一个细微的差异:如果可能的话,它们被推断为其值的缩小字面量类型,而不是更宽泛的原始类型。TypeScript 对更强烈的初始类型缩小感到放心,因为它知道值不会被稍后改变;它类似于 const 变量比 let 变量具有更窄的类型。

在此示例中,类属性最初都声明为字符串字面量,因此为了将其中一个扩展为 string,需要类型注解:

class RandomQuote {
  readonly explicit: string = "Home is the nicest word there is.";
  readonly implicit = "Home is the nicest word there is.";

  constructor() {
    if (Math.random() > 0.5) {
      this.explicit = "We start learning the minute we're born." // Ok;

      this.implicit = "We start learning the minute we're born.";
      // 错误:不能将类型“"We start learning the minute we're born."”
      // 分配给类型“"Home is the nicest word there is."”
    }
  }
}

const quote = new RandomQuote();

quote.explicit; // Type: string
quote.implicit; // Type: "Home is the nicest word there is."

通常不需要显式扩展属性的类型。尽管如此,它有时在构造函数中的条件逻辑(如 RandomQuote 中)的情况下很有用。

类作为类型

类在类型系统中相对唯一,因为类声明既创建运行时值(类本身)又创建可在类型注解中使用的类型。

这个 Teacher 类的名称用于注解 teacher 变量,告诉 TypeScript 应该只给它分配可分配给 Teacher 类的值,例如 Teacher 类本身的实例:

class Teacher {
  sayHello() {
    console.log("Take chances, make mistakes, get messy!");
  }
}

let teacher: Teacher;

teacher = new Teacher(); // Ok

teacher = "Wahoo!";
// 错误:不能将类型“string”分配给类型“Teacher”。

有趣的是,TypeScript 将包含与类完全相同成员的任何对象类型视为可分配给该类。这是因为 TypeScript 的结构类型只关心对象的形状,而不关心它们如何声明。

在这里,withSchoolBus 接受一个 SchoolBus 类型的参数。任何具有 getAbilities 属性(类型为()=> string[])的对象都可以满足此要求,比如 SchoolBus 类的实例:

class SchoolBus {
  getAbilities() {
    return ["magic", "shapeshifting"];
  }
}

function withSchoolBus(bus: SchoolBus) {
  console.log(bus.getAbilities());
}

withSchoolBus(new SchoolBus()); // Ok

// Ok
withSchoolBus({
  getAbilities: () => ["transmogrification"],
});

withSchoolBus({
  getAbilities: () => 123,
  //    ~~~
  // 错误:不能将类型“number”分配给类型“string[]”。
});
在大多数实际代码中,开发人员不会在请求类类型的地方传递对象值。这种结构检查行为可能看起来出乎意料,但并不经常出现。

类和接口

回到第 7 章 “接口”,我向你展示了接口如何允许 TypeScript 开发人员为代码中设置对象形状的期望。TypeScript 允许类通过在类名后添加 implements 关键字,后面跟一个接口的名称,将其实例声明为符合接口规范。这表明 TypeScript 应该将该类的实例分配给每个接口。任何不匹配的情况都将被类型检查器指出为类型错误。

在这个例子中,Student 类通过包含其属性 name 和方法 study,正确实现了 Learner 接口,但 Slacker 中没有 study 方法,因此导致类型错误:

interface Learner {
  name: string;
  study(hours: number): void;
}

class Student implements Learner {
  name: string;

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

  study(hours: number) {
    for (let i = 0; i < hours; i += 1) {
      console.log("...studying...");
    }
  }
}

class Slacker implements Learner {
  // ~~~~~~~
  // 错误:类“Slacker”错误实现接口“Learner”。
  //   类型 "Slacker" 中缺少属性 "study",
  //   但类型 "Learner" 中需要该属性。
  name = "Rocky";
}
通常使用方法语法来声明接口成员作为函数(如 Learner 接口所用)是实现类的一个典型原因。

标记一个类实现接口不会更改有关类使用方式的任何内容。如果该类已经与接口匹配,TypeScript 的类型检查器将允许在需要接口实例的地方使用其实例。TypeScript 甚至不会从接口推断类上的方法或属性的类型:如果我们在 Slacker 示例中添加了 study(hours) {} 方法,TypeScript 会将 hours 参数视为隐式 any,除非我们给它一个类型注解。

这个Student类的版本会导致隐式 any 类型错误,因为它没有为成员提供类型注解:

class Student implements Learner {
  name;
  // 错误:成员“name”隐式包含类型“any”。

  study(hours) {
    // 错误:参数“hours”隐式具有“any”类型。
  }
}

实现接口纯粹是一种安全检查。它不会将任何接口成员复制到类定义中。相反,实现接口会向类型检查器发出信号,并在类定义中显示类型错误,而不是稍后使用类实例。它的目的类似于向变量添加类型注解,即使它具有初始值。

实现多个接口

TypeScript 中的类被允许声明为实现多个接口。类的已实现接口列表可以是任意数量的接口名称,中间带有逗号。

在这个例子中,两个类都需要至少有一个 grades 属性来实现 Graded, 一个 report 属性来实现 ReporterEmpty 类有两个类型错误,因为没有正确实现任何一个接口:

interface Graded {
  grades: number[];
}

interface Reporter {
  report: () => string;
}

class ReportCard implements Graded, Reporter {
  grades: number[];

  constructor(grades: number[]) {
    this.grades = grades;
  }

  report() {
    return this.grades.join(", ");
  }
}

class Empty implements Graded, Reporter 
// ~~~~~
// 错误:类“Empty”错误实现接口“Graded”。
//   类型 "Empty" 中缺少属性 "grades",但类型 "Graded" 中需要该属性。
// 错误:类“Empty”错误实现接口“Reporter”。
//   类型 "Empty" 中缺少属性 "report",但类型 "Reporter" 中需要该属性。

在实践中,可能会有一些接口的定义使得一个类实现两个接口成为不可能。试图声明一个实现两个冲突接口的类将导致该类至少出现一个类型错误。

以下 AgeIsANumberAgeIsNotANumber 接口为 age 属性声明了非常不同的类型。既不是 AsNumber 类也不是 NotAsNumber 类能够同时正确地实现两者:

interface AgeIsANumber {
  age: number;
}

interface AgeIsNotANumber {
  age: () => string;
}

class AsNumber implements AgeIsANumber, AgeIsNotANumber {
  age = 0;
  // ~~~
  // 错误:类型“AsNumber”中的属性“age”不可分配给基类型“AgeIsNotANumber”中的同一属性。
  //   不能将类型“number”分配给类型“() => string”。
}

class NotAsNumber implements AgeIsANumber, AgeIsNotANumber {
  age() { return ""; }
  // ~~~
  // 错误:类型“NotAsNumber”中的属性“age”不可分配给基类型“AgeIsANumber”中的同一属性。
  //   不能将类型“() => string”分配给类型“number”。ts(2416)
}

两个接口描述非常不同的对象形状的情况通常表示你不应尝试使用相同的类实现它们。

扩展类

TypeScript 在 JavaScript 的类扩展(或子类化)的概念上添加了类型检查。首先,在基类上声明的任何方法或属性都将在子类(也称为派生类)上可用。

在这个例子中,Teacher 声明了一个 teach 方法,可以由 StudentTeacher 子类的实例使用:

class Teacher {
  teach() {
    console.log("The surest test of discipline is its absence.");
  }
}

class StudentTeacher extends Teacher {
  learn() {
    console.log("I cannot afford the luxury of a closed mind.");
  }
}

const teacher = new StudentTeacher(); 

teacher.teach(); // Ok (defined on base) teacher.learn(); // Ok (defined on subclass)

teacher.other();
//    ~~~~~
// 错误:类型“StudentTeacher”上不存在属性“other”。

扩展可分配性

子类从其基类继承成员,就像派生接口继承基础接口一样。子类的实例具有其基类的所有成员,因此可以在需要基类实例的任何地方使用子类实例。如果基类没有子类的所有成员,则在需要更具体的子类时,无法使用基类。

以下 Lesson 类的实例不能在需要派生的 OnlineLesson 实例的地方使用,但是派生实例可以用来满足基类或子类的要求:

class Lesson {
  subject: string;

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

class OnlineLesson extends Lesson {
  url: string;

  constructor(subject: string, url: string) {
    super(subject); this.url = url;
  }
}

let lesson: Lesson;
lesson = new Lesson("coding"); // Ok
lesson = new OnlineLesson("coding", "oreilly.com"); // Ok

let online: OnlineLesson;
online = new OnlineLesson("coding", "oreilly.com"); // Ok

online = new Lesson("coding");
// 错误:类型 "Lesson" 中缺少属性 "url",
// 但类型 "OnlineLesson" 中需要该属性。

根据 TypeScript 的结构类型,如果子类中的所有成员在其基类中以相同类型存在,则仍允许使用基类的实例来替代子类。

在这个例子中,LabeledPastGrades 只添加了一个可选属性到 PastGrades,因此可以使用基类的实例来替代子类。

class PastGrades {
  grades: number[] = [];
}

class LabeledPastGrades extends PastGrades {
  label?: string;
}

let subClass: LabeledPastGrades;

subClass = new LabeledPastGrades(); // Ok
subClass = new PastGrades(); // Ok
在大多数实际代码中,子类通常会在其基类之上添加新的必需类型信息。这种结构检查行为可能看起来出乎意料,但并不经常出现。

重写构造函数

与原生 JavaScript 一样,TypeScript 的子类不需要定义自己的构造函数。没有自己的构造函数的子类隐式使用其基类中的构造函数。

在 JavaScript 中,如果子类确实声明了自己的构造函数,则必须通过 super 关键字调用其基类构造函数。子类构造函数可以声明任何参数,而不管其基类需要什么参数。TypeScript 的类型检查器将确保对基类构造函数的调用使用正确的参数。

在这个例子中,PassingAnnouncer 的构造函数使用 number 参数正确调用了基类构造函数,而 FailingAnnouncer 由于忘记该调用而抛出类型错误:

class GradeAnnouncer { 
  message: string;

  constructor(grade: number) {
    this.message = grade >= 65 ? "Maybe next time..." : "You pass!";
  }
}

class PassingAnnouncer extends GradeAnnouncer {
  constructor() {
    super(100);
  }
}

class FailingAnnouncer extends GradeAnnouncer {
  constructor() 
  // ~~~~~~~~~~~~~~~~~
  // 错误:派生类的构造函数必须包含 "super" 调用。
}

根据 JavaScript 规则,子类的构造函数必须在访问 thissuper 之前调用基构造函数。如果 TypeScript 在 super() 之前看到访问的 thissuper,它将报告类型错误。

以下 ContinuedGradesTally 类在调用 super() 之前在其构造函数中错误地引用了 this.grades

class GradesTally {
  grades: number[] = [];

  addGrades(...grades: number[]) {
    this.grades.push(...grades); return this.grades.length;
  }
}

class ContinuedGradesTally extends GradesTally {
  constructor(previousGrades: number[]) {
    this.grades = [...previousGrades];
    // 错误:访问派生类的构造函数中的 "this" 前,必须调用 "super"。

    super();

    console.log("Starting with length", this.grades.length); // Ok
  }
}

重写方法

子类可以重新声明与基类同名的新方法,只要子类方法可以分配给基类方法即可。请记住,由于子类可以用于任何原始类可以使用的地方,因此新方法的类型必须可以替换原始方法的类型。

在这个例子中,FailureCounter 类的 countGrades 方法是允许的,因为它具有与基类 GradeCountercountGrades 方法相同的第一个参数和返回类型。

AnyFailureChecker 类的 countGrades 方法由于具有错误的返回类型而导致类型错误:

class GradeCounter {
  countGrades(grades: string[], letter: string) {
    return grades.filter(grade => grade === letter).length;
  }
}
class FailureCounter extends GradeCounter {
  countGrades(grades: string[]) {
    return super.countGrades(grades, "F");
  }
}
class AnyFailureChecker extends GradeCounter {
  countGrades(grades: string[]) {
    // 类型“AnyFailureChecker”中的属性“countGrades”不可分配给基类型“GradeCounter”中的同一属性。
    //   不能将类型“(grades: string[]) => boolean”分配给类型“(grades: string[], letter: string) => number”。
    //     不能将类型“boolean”分配给类型“number”。
    return super.countGrades(grades, "F") !== 0;
  }
}
const counter: GradeCounter = new AnyFailureChecker();
// 期望类型:number
// 实际类型:boolean
const count = counter.countGrades(["A", "C", "F"]);

重写属性

子类还可以显式地重新声明具有相同名称的基类的属性,只要新类型可分配给基类上的类型即可。与重写方法一样,子类必须在结构上与基类匹配。

大多数重新声明属性的子类这样做,要么是为了使这些属性成为类型联合的更具体的子集,要么使这些属性成为扩展基类属性类型的类型。

在这个例子中,基类 Assignment 声明其 gradenumber | undefined,而子类 GradedAssignment 将其声明为必须始终存在的 number

class Assignment {
  grade?: number;
}

class GradedAssignment extends Assignment {
  grade: number;

  constructor(grade: number) {
    super();
    this.grade = grade;
  }
}

将属性的联合类型中允许的值扩展到其他类型是不允许的,因为这样会使子类的属性无法再分配给基类属性的类型。

在此示例中,VagueGradevalue 试图在基类 NumericGradenumber 类型上添加 | string,导致了类型错误:

class NumericGrade {
  value = 0;
}

class VagueGrade extends NumericGrade {
  value = Math.random() > 0.5 ? 1 : "...";

  // 错误:类型“VagueGrade”中的属性“value”不可分配给基类型“NumericGrade”中的同一属性。
  //   不能将类型“string | number”分配给类型“number”。
  //     不能将类型“string”分配给类型“number”。
}

const instance: NumericGrade = new VagueGrade();

// 期望类型:number
// 实际类型:number | string
instance.value;

抽象类

有时候,创建一个基类是很有用的,它本身不声明某些方法的实现,而是期望子类提供给它们。将类标记为抽象是通过在类名前面和任何要抽象的方法前面添加 TypeScript 的 abstract 关键字来完成的。这些抽象方法声明在抽象基类中不是提供实现,而是声明与接口相同的方式。

在这个例子中,School 类及其 getStudentTypes 方法标记为 abstract。因此,它的子类 —— PreschoolAbsence 需要实现 getStudentTypes 方法:

abstract class School {
  readonly name: string;

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

  abstract getStudentTypes(): string[];
}

class Preschool extends School {
  getStudentTypes() {
    return ["preschooler"];
  }
}

class Absence extends School 
// ~~~~~~~
// 错误:非抽象类“Absence”不会实现继承自“School”类的抽象成员“getStudentTypes”。

抽象类不能直接实例化,因为它没有其实现可能假定确实存在的某些方法的定义。只能实例化非抽象(“具体”)类。

继续 School 示例,尝试调用 new School 将导致 TypeScript 类型错误:

let school: School;

school = new Preschool("Sunnyside Daycare"); // Ok

school = new School("somewhere else");
// 错误:无法创建抽象类的实例。

抽象类经常被用于框架中,消费者需要填写类的细节。该类可以用作类型注解,表示值必须符合该类——如前面的 school:School 示例一样,但是必须使用子类创建新实例。

成员可见性

JavaScript 具有将类成员的名称以 # 开头标记为“私有”类成员的功能。私有类成员只能由该类的实例访问。JavaScript 运行时通过在类外部的代码尝试访问私有方法或属性时抛出错误来强制实施该隐私。

TypeScript 的类支持早于 JavaScript 真正的 # 隐私,虽然 TypeScript 支持私有类成员,但它也允许对仅存在于类型系统中的类方法和属性上使用更细微的隐私定义。TypeScript 的成员可见性是通过在类成员的声明名称之前添加以下关键字之一来实现的:

  • public (默认)
    允许任何人在任何地方访问
  • protected
    仅允许类本身及其子类访问
  • private
    仅允许类本身访问访问

这些关键字纯粹存在于类型系统中。当代码编译为 JavaScript 时,它们与所有其他类型系统语法一起删除。

在这里,Base 声明了两个 public 成员,一个 protected,一个 private,还有一个真正的私有成员 #truePrivate。允许 Subclass 访问 publicprotected 成员,但不允许访问 private#truePrivate

class Base {
  isPublicImplicit = 0;
  public isPublicExplicit = 1; 
  protected isProtected = 2; 
  private isPrivate = 3; 
  #truePrivate = 4;
}

class Subclass extends Base {
  examples() {
    this.isPublicImplicit; // Ok this.isPublicExplicit; // Ok this.isProtected; // Ok

    this.isPrivate;
    // 错误:属性“isPrivate”为私有属性,只能在类“Base”中访问。

    this.#truePrivate;
    // 属性 "#truePrivate" 在类 "Base" 外部不可访问,因为它具有专用标识符。
  }
}

new Subclass().isPublicImplicit; // Ok
new Subclass().isPublicExplicit; // Ok

new Subclass().isProtected;
//    ~~~~~~~~~~~
// 错误:属性“isProtected”受保护,只能在类“Base”及其子类中访问。

new Subclass().isPrivate;
//    ~~~~~~~~~~~
// 错误:属性“isPrivate”为私有属性,只能在类“Base”中访问。

TypeScript 的成员可见性和 JavaScript 的真正私有声明之间的主要区别在于,TypeScript 只存在于类型系统中,而 JavaScript 也存在于运行时。声明为 protectedprivate 的 TypeScript 类成员将编译为相同的 JavaScript 代码,就好像它们被显式或隐式声明为 public 一样。与接口和类型注解一样,可见性关键字在输出 JavaScript 时会被删除。只有 # 私有字段在运行时 JavaScript 中是真正私有的。

可见性修饰符可以与 readonly 一起标记。要将成员声明为 readonly 并具有显式可见性,则先声明可见性。

TwoKeywords 类将其 name 成员声明为 privatereadonly

class TwoKeywords {
  private readonly name: string;

  constructor() {
    this.name = "Anne Sullivan"; // Ok
  }

  log() {
    console.log(this.name); // Ok
  }
}

const two = new TwoKeywords();

two.name = "Savitribai Phule";
// ~~~~
// 错误:属性“name”为私有属性,只能在类“TwoKeywords”中访问。
// 无法为“name”赋值,因为它是只读属性。ts(2540)

请注意,不允许将 TypeScript 的旧成员可见性关键字与 JavaScript 的新 # 私有字段混合使用。默认情况下,私有字段始终是私有的,因此无需使用 private 关键字额外标记它们。

静态字段修饰符

JavaScript 允许在类本身上声明成员——而不是它的实例——使用 static 关键字。TypeScript 支持单独使用 static 关键字一起使用,或者与 readonly,或者与见性关键字之——起使用。组合在一起时,可见性关键字首先出现,然后是 static,然后是 readonly

这个 HasStatic 类将它们组合在一起,使其 static promptanswer属性同时具有 readonlyprotected

class Question {
  protected static readonly answer: "bash";
  protected static readonly prompt =
    "What's an ogre's favorite programming language?";

  guess(getAnswer: (prompt: string) => string) {
    const answer = getAnswer(Question.prompt);

    // Ok
    if (answer === Question.answer) {
      console.log("You got it!");
    } else {
      console.log("Try again...")
    }
  }
}

Question.answer;
//    ~~~~~~
// 错误:属性“answer”受保护,只能在类“Question”及其子类中访问。

使用只读和/或可见性修饰符来限制静态类字段在类外部被访问或修改是很有用的。这有助于确保代码的正确性和可维护性。

总结

本章介绍了大量围绕类的类型系统特性和语法:

  • 声明和使用的类方法和属性
  • 标记属性只读可选
  • 在类型注解中使用类名作为类型
  • 实现接口以强制执行类实例形状
  • 扩展类,以及子类的可分配性和覆盖规则
  • 将类和方法标记为抽象
  • 向类字段添加类型系统修饰符
*现在你已经读完了这一章,你最好练习一下学到的东西 https://learningtypescript.com/classes

为什么面向对象编程开发人员总是穿西装?
因为他们有格调。

0

评论 (0)

取消