【TypeScript系列】类

背景阅读:
类(MDN)

TypeScript 对 ES2015 引入的 class 关键字提供了全面支持。

与其他 JavaScript 语言特性一样,TypeScript 添加了类型注解和其他语法,使你能够表达类和其他类型之间的关系。

类成员

下面是一个最基本的类——空类:

class Point {}

这个类目前还没有什么用,所以我们添加一些成员。

字段

字段声明在类上创建了一个公共可写属性:

// @strictPropertyInitialization: false
class Point {x: number;y: number;
}const pt = new Point();
pt.x = 0;
pt.y = 0;

与其他位置一样,类型注解是可选的,但如果未指定,则会隐式为 any 类型。

字段还可以有_初始化器_;当类被实例化时,它们将自动运行:

class Point {x = 0;y = 0;
}const pt = new Point();
// 输出 0, 0
console.log(`${pt.x}, ${pt.y}`);

constletvar 一样,类属性的初始化器会用于推断其类型:

// @errors: 2322
class Point {x = 0;y = 0;
}
// ---cut---
const pt = new Point();
pt.x = "0";
--strictPropertyInitialization

strictPropertyInitialization 设置项控制是否需要在构造函数中初始化类字段。

// @errors: 2564
class BadGreeter {name: string;
}
class GoodGreeter {name: string;constructor() {this.name = "hello";}
}

请注意,字段需要在_构造函数内部_进行初始化。TypeScript 在检测初始化时不会分析从构造函数中调用的方法,因为派生类可能会覆写这些方法,并未初始化成员。

如果你打算通过构造函数以外的方式明确初始化字段(例如,也许外部库填充类的一部分内容),你可以使用_明确赋值断言操作符_ !

class OKGreeter {// 没有初始化,但没有错误name!: string;
}

readonly

字段可以使用 readonly 修饰符进行前缀标记。这将阻止在构造函数之外对字段进行赋值。

// @errors: 2540 2540
class Greeter {readonly name: string = "world";constructor(otherName?: string) {if (otherName !== undefined) {this.name = otherName;}}err() {this.name = "不可以";}
}
const g = new Greeter();
g.name = "同样不可以";

构造函数

背景阅读:
构造函数(MDN)

类构造函数与函数非常相似。你可以添加带有类型注解、默认值和重载的参数:

class Point {x: number;y: number;// 带有默认值的普通签名constructor(x = 0, y = 0) {this.x = x;this.y = y;}
}
class Point {// 重载constructor(x: number, y: string);constructor(s: string);constructor(xs: any, y?: any) {// 待定}
}

类构造函数签名与函数签名之间只有一些小的区别:

  • 构造函数不能有类型参数——这些属于外部类声明的部分,我们稍后会学习到
  • 构造函数不能有返回类型注解——返回的类型始终是类实例的类型
调用父类构造函数

与 JavaScript 类似,如果你有一个基类,在使用任何 this. 成员之前,需要在构造函数体中调用 super();

// @errors: 17009
class Base {k = 4;
}class Derived extends Base {constructor() {// 在 ES5 中输出错误的值;在 ES6 中抛出异常console.log(this.k);super();}
}

忘记调用 super 是在 JavaScript 中很容易犯的错误,但是 TypeScript 会在必要时告诉你。

方法

背景阅读:
方法定义

类中的函数属性称为_方法_。方法可以使用与函数和构造函数相同的类型注解:

class Point {x = 10;y = 10;scale(n: number): void {this.x *= n;this.y *= n;}
}

除了标准的类型注解外,TypeScript 对方法没有引入任何新的内容。

请注意,在方法体内部,仍然必须通过 this. 访问字段和其他方法。在方法体中的未限定名称始终会引用封闭作用域中的某个内容:

// @errors: 2322
let x: number = 0;class C {x: string = "hello";m() {// 这是尝试修改第 1 行的‘x’,而不是类属性x = "world";}
}

Getter / Setter

类也可以拥有_访问器_:

class C {_length = 0;get length() {return this._length;}set length(value) {this._length = value;}
}

注意,在 JavaScript 中,如果一个字段的 get/set 对没有额外的逻辑,它很少有用处。
如果在 get/set 操作期间不需要添加其他逻辑,可以直接暴露公共字段。

TypeScript 对访问器有一些特殊的类型推断规则:

  • 如果存在 get 但不存在 set,则属性自动为 readonly
  • 如果 setter 参数的类型未指定,则从 getter 的返回类型进行推断
  • getter 和 setter 的成员可见性必须相同

自 TypeScript 4.3 起,可以在获取和设置时使用不同的类型。

class Thing {_size = 0;get size(): number {return this._size;}set size(value: string | number | boolean) {let num = Number(value);// 不允许 NaN、Infinity 等if (!Number.isFinite(num)) {this._size = 0;return;}this._size = num;}
}

索引签名

类可以声明索引签名;这与其他对象类型的索引签名相同:

class MyClass {[s: string]: boolean | ((s: string) => boolean);check(s: string) {return this[s] as boolean;}
}

由于索引签名类型还需要捕获方法的类型,因此很难有用地使用这些类型。通常最好将索引数据存储在类实例本身以外的其他位置。

类继承

与其他具有面向对象特性的语言一样,JavaScript 中的类可以从基类继承。

implements 子句

你可以使用 implements 子句来检查一个类是否满足特定的接口。如果类未能正确实现接口,将会发出错误提示:

// @errors: 2420
interface Pingable {ping(): void;
}class Sonar implements Pingable {ping() {console.log("ping!");}
}class Ball implements Pingable {pong() {console.log("pong!");}
}

类也可以实现多个接口,例如 class C implements A, B {

注意事项

重要的是要理解,implements 子句仅仅是一个检查,用于判断类是否可以被视为接口类型。它_完全_不会改变类或其方法的类型。一个常见的错误是认为 implements 子句会改变类的类型——实际上并不会!

// @errors: 7006
interface Checkable {check(name: string): boolean;
}class NameChecker implements Checkable {check(s) {// 注意这里没有错误return s.toLowerCase() === "ok";//         ^?}
}

在这个例子中,我们可能认为 s 的类型会受到 check 方法的 name: string 参数的影响。但实际上不会——implements 子句不会改变对类的检查或类型推断的方式。

类似地,实现具有可选属性的接口并不会创建该属性:

// @errors: 2339
interface A {x: number;y?: number;
}
class C implements A {x = 0;
}
const c = new C();
c.y = 10;

extends 子句

背景阅读:
extends 关键字(MDN)

类可以从基类进行 extends 继承。派生类具有其基类的所有属性和方法,并且还可以定义额外的成员。

class Animal {move() {console.log("继续前进!");}
}class Dog extends Animal {woof(times: number) {for (let i = 0; i < times; i++) {console.log("汪!");}}
}const d = new Dog();
// 基类方法
d.move();
// 派生类方法
d.woof(3);
覆写方法

背景阅读:
super 关键字(MDN)

派生类还可以覆写基类的字段或属性。你可以使用 super. 语法来访问基类的方法。请注意,由于 JavaScript 类是一个简单的查找对象,没有“super 字段”的概念。

TypeScript 强制要求派生类始终是其基类的子类型。

例如,以下是一种覆写方法的合法方式:

class Base {greet() {console.log("你好,世界!");}
}class Derived extends Base {greet(name?: string) {if (name === undefined) {super.greet();} else {console.log(`你好,${name.toUpperCase()}`);}}
}const d = new Derived();
d.greet();
d.greet("reader");

派生类必须遵循其基类的约定。请记住,通过基类引用来引用派生类实例是非常常见的做法(并且始终是合法的):

class Base {greet() {console.log("你好,世界!");}
}
class Derived extends Base {}
const d = new Derived();
// ---cut---
// 通过基类引用来声明派生类实例的别名
const b: Base = d;
// 没有问题
b.greet();

那么如果 Derived 不遵循 Base 的约定会怎样呢?

// @errors: 2416
class Base {greet() {console.log("你好,世界!");}
}class Derived extends Base {// 使这个参数成为必需的greet(name: string) {console.log(`你好,${name.toUpperCase()}`);}
}

如果我们无视错误,仍编译了这段代码,那么这个示例将会崩溃:

declare class Base {greet(): void;
}
declare class Derived extends Base {}
// ---cut---
const b: Base = new Derived();
// 由于“name”为 undefined,所以崩溃
b.greet();
仅类型字段声明

target >= ES2022useDefineForClassFieldstrue 时,类字段在父类构造函数完成后进行初始化,覆盖了父类设置的任何值。这在你只想为继承字段声明更准确的类型时可能会成为问题。为了处理这些情况,你可以使用 declare 来告诉 TypeScript 此字段声明不会产生运行时效果。

interface Animal {dateOfBirth: any;
}interface Dog extends Animal {breed: any;
}class AnimalHouse {resident: Animal;constructor(animal: Animal) {this.resident = animal;}
}class DogHouse extends AnimalHouse {// 不会生成 JavaScript 代码,// 只是确保类型正确declare resident: Dog;constructor(dog: Dog) {super(dog);}
}
初始化顺序

JavaScript 类的初始化顺序在某些情况下可能会出人意料。让我们来看一下这段代码:

class Base {name = "基础";constructor() {console.log("我是" + this.name);}
}class Derived extends Base {name = "派生";
}// 输出“基础”,而不是“派生”
const d = new Derived();

怎么回事?

按照 JavaScript 的定义,类的初始化顺序如下:

  • 初始化基类字段
  • 运行基类构造函数
  • 初始化派生类字段
  • 运行派生类构造函数

这意味着在运行基类构造函数期间,它看到的是自己的 name 值,因为派生类字段的初始化尚未运行。

继承内置类型

注意:如果你不打算继承像 ArrayErrorMap 等内置类型,或者你的编译目标明确设置为 ES6/ES2015 或更高版本,则可以跳过本节。

在 ES2015 中,返回对象的构造函数会隐式地用 this 的值替换 super(...) 的调用者。生成的构造函数代码需要捕获 super(...) 的潜在返回值并将其替换为 this

因此,ErrorArray 等类型的继承可能不再按预期工作。这是因为 ErrorArray 等类型的构造函数使用 ECMAScript 6 的 new.target 来调整原型链;然而,在 ECMAScript 5 中,在调用构造函数时无法确保为 new.target 设置一个值。其他降级编译器通常都有相同的限制。

对于以下子类:

class MsgError extends Error {constructor(m: string) {super(m);}sayHello() {return "你好" + this.message;}
}

你可能会发现:

  • 在构造这些子类的对象时,方法可能为 undefined,因此调用 sayHello 将导致错误。
  • 子类实例与其实例之间的 instanceof 将会失效,因此 (new MsgError()) instanceof MsgError 将返回 false

作为建议,你可以在任何 super(...) 调用之后手动调整原型。

class MsgError extends Error {constructor(m: string) {super(m);// 显式地设置原型。Object.setPrototypeOf(this, MsgError.prototype);}sayHello() {return "hello " + this.message;}
}

但是,MsgError 的任何子类也必须手动设置原型。对于不支持 Object.setPrototypeOf 的运行时环境,你可以使用 __proto__

不幸的是,这些解决方法在 Internet Explorer 10 及更早版本中不起作用。你可以手动将原型上的方法复制到实例本身(即将 MsgError.prototype 复制到 this),但原型链本身无法修复。

成员可见性

你可以使用 TypeScript 控制某些方法或属性对类外部代码的可见性。

public

类成员的默认可见性是 publicpublic 成员可以在任何地方访问:

class Greeter {public greet() {console.log("嗨!");}
}
const g = new Greeter();
g.greet();

因为 public 已经是默认的可见性修饰符,所以你不需要在类成员上写它,但出于风格或可读性的原因,你可以选择这样做。

protected

protected 成员只对声明它们的类的子类可见。

// @errors: 2445
class Greeter {public greet() {console.log("你好," + this.getName());}protected getName() {return "嗨";}
}class SpecialGreeter extends Greeter {public howdy() {// 可以在这里访问受保护的成员console.log("Howdy, " + this.getName());//                          ^^^^^^^^^^^^^^}
}
const g = new SpecialGreeter();
g.greet(); // 可以
g.getName();
暴露 protected 成员

派生类需要遵循其基类的约定,但可以选择暴露具有更多功能的基类子类型。这包括将 protected 成员改为 public

class Base {protected m = 10;
}
class Derived extends Base {// 没有修饰符,所以默认是‘public’m = 15;
}
const d = new Derived();
console.log(d.m); // 可以

请注意,Derived 已经能够自由读取和写入 m,因此这种情况下并不会实质性地改变“安全性”。这里需要注意的主要事项是,在派生类中,如果这种暴露不是有意的,请小心重复使用 protected 修饰符。

跨层级的 protected 访问

不同的面向对象编程语言对于通过基类引用访问 protected 成员是否合法存在分歧:

// @errors: 2446
class Base {protected x: number = 1;
}
class Derived1 extends Base {protected x: number = 5;
}
class Derived2 extends Base {f1(other: Derived2) {other.x = 10;}f2(other: Derived1) {other.x = 10;}
}

例如,Java 认为这是合法的。另一方面,C# 和 C++ 则认为这段代码应该是非法的。

TypeScript 在这里与 C# 和 C++ 保持一致,因为只有从 Derived2 的子类中才能合法地访问 Derived2 中的 x,而 Derived1 不是其中之一。此外,如果通过 Derived1 引用访问 x 是非法的(肯定应该是!),那么通过基类引用访问它是无法改变这种情况的。

参见为何不能访问派生类的 Protected 成员?,其中更详细地解释了 C# 的原理。

private

privateprotected 类似,但甚至不允许从子类中访问该成员:

// @errors: 2341
class Base {private x = 0;
}
const b = new Base();
// 无法从类外部访问
console.log(b.x);
// @errors: 2341
class Base {private x = 0;
}
// ---cut---
class Derived extends Base {showX() {// 无法在子类中访问console.log(this.x);}
}

由于 private 成员对派生类不可见,派生类无法增加它们的可见性:

// @errors: 2415
class Base {private x = 0;
}
class Derived extends Base {x = 1;
}
跨实例的 private 访问

不同的面向对象编程语言在是否允许同一类的不同实例访问彼此的 private 成员上存在分歧。Java、C#、C++、Swift 和 PHP 等语言允许这样做,但 Ruby 不允许。

TypeScript 允许跨实例的 private 访问:

class A {private x = 10;public sameAs(other: A) {// 没有错误return other.x === this.x;}
}
注意事项

与 TypeScript 类型系统的其他方面一样,privateprotected 仅在类型检查期间执行。

这意味着像 in 或简单的属性查询这样的 JavaScript 运行时构造仍然可以访问 privateprotected 成员:

class MySafe {private secretKey = 12345;
}
// 在 JavaScript 文件中...
const s = new MySafe();
// 将打印 12345
console.log(s.secretKey);

private 还允许在类型检查期间使用方括号表示法进行访问。这使得对 private 声明的字段的访问在单元测试等情况下更容易,缺点是这些字段是_软私有的_,不严格执行私有化。

// @errors: 2341
class MySafe {private secretKey = 12345;
}const s = new MySafe();// 在类型检查期间不允许
console.log(s.secretKey);// 可以
console.log(s["secretKey"]);

与 TypeScript 的 private 不同,JavaScript 的私有字段(#)在编译后仍然保持私有,并且不提供先前提到的方括号访问等逃逸口,使得它们成为_硬私有_字段。

class Dog {#barkAmount = 0;personality = "happy";constructor() {}
}
// @target: esnext
// @showEmit
class Dog {#barkAmount = 0;personality = "happy";constructor() {}
}

当编译为 ES2021 或更低版本时,TypeScript 将使用 WeakMaps 代替 #

// @target: es2015
// @showEmit
class Dog{#barkAmount = 0;personality = "happy";constructor() {}
}

如果你需要保护类中的值免受恶意操作,你应该使用提供硬运行时私有的机制,例如闭包、WeakMaps 或私有字段。请注意,这些在运行时添加的隐私检查可能会影响性能。

静态成员

背景阅读
静态成员(MDN)

类可以具有 static 成员。这些成员不与类的特定实例关联。可以通过类构造器对象本身访问它们:

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

静态成员也可以使用相同的 publicprotectedprivate 可见性修饰符:

// @errors: 2341
class MyClass {private static x = 0;
}
console.log(MyClass.x);

静态成员也会被继承:

class Base {static getGreeting() {return "你好世界";}
}
class Derived extends Base {myGreeting = Derived.getGreeting();
}

特殊的静态名称

通常情况下,覆盖 Function 原型的属性是不安全/不可能的。由于类本身是可以使用 new 调用的函数,因此不能使用某些静态名称。诸如 namelengthcall 的函数属性不能作为 static 成员定义:

// @errors: 2699
class S {static name = "S!";
}

为什么没有静态类?

TypeScript(以及 JavaScript)没有类似于 C# 的 static class 构造。

这些构造的存在是因为这些语言强制要求所有的数据和函数都在类内部;因为 TypeScript 中不存在这种限制,所以也就没有必要使用它们。在 JavaScript/TypeScript 中,通常将只有一个实例的类表示为普通的_对象_。

例如,在 TypeScript 中我们不需要“static class”语法,因为普通的对象(甚至是顶级函数)同样可以完成工作:

// 不必要的“static”类
class MyStaticClass {static doSomething() {}
}// 首选(替代方案 1)
function doSomething() {}// 首选(替代方案 2)
const MyHelperObject = {dosomething() {},
};

类中的 static

静态块允许你编写一系列具有自己作用域的语句,这些语句可以访问包含类中的私有字段。这意味着我们可以编写具有所有语句编写功能、没有变量泄漏以及对类内部的完全访问权限的初始化代码。

declare function loadLastInstances(): any[]
// ---cut---
class Foo {static #count = 0;get count() {return Foo.#count;}static {try {const lastInstances = loadLastInstances();Foo.#count += lastInstances.length;}catch {}}
}

泛型类

类,类似于接口,可以是泛型的。当使用 new 实例化泛型类时,其类型参数的推断方式与函数调用相同:

class Box<Type> {contents: Type;constructor(value: Type) {this.contents = value;}
}const b = new Box("你好!");
//    ^?

类可以像接口一样使用泛型约束和默认值。

静态成员中的类型参数

下面的代码是不合法的,可能并不明显为什么会出错:

// @errors: 2302
class Box<Type> {static defaultValue: Type;
}

请记住,类型始终会完全擦除!在运行时,只有一个 Box.defaultValue 属性槽位。这意味着设置 Box<string>.defaultValue(如果可能的话)也会同时改变 Box<number>.defaultValue——不太好。泛型类的 static 成员永远不能引用类的类型参数。

类的运行时 this

背景阅读
this 关键字(MDN)

请记住,TypeScript 不会改变 JavaScript 的运行时行为,而 JavaScript 因其某些怪异的运行时行为而有一定的“声名”。

JavaScript 对 this 的处理方式确实有些不寻常:

class MyClass {name = "MyClass";getName() {return this.name;}
}
const c = new MyClass();
const obj = {name: "obj",getName: c.getName,
};// 输出“obj”,而不是“MyClass”
console.log(obj.getName());

简而言之,默认情况下,函数内部的 this 值取决于函数的调用方式。在这个例子中,因为函数是通过 obj 引用调用的,它的 this 值是 obj 而不是类的实例。

这通常不是你想要的结果!TypeScript 提供了一些方法来减轻或防止这种类型的错误。

箭头函数

背景阅读:
箭头函数(MDN)

如果一个函数在调用时经常丢失其 this 上下文,那么使用箭头函数属性而不是方法定义可能是有意义的:

class MyClass {name = "MyClass";getName = () => {return this.name;};
}
const c = new MyClass();
const g = c.getName;
// 输出“MyClass”而不会崩溃
console.log(g());

这种方式有一些权衡:

  • this 值在运行时保证是正确的,即使对于未经 TypeScript 检查的代码也是如此。
  • 这会使用更多的内存,因为每个类实例都会有自己的这种方式定义的函数副本。
  • 无法在派生类中使用 super.getName,因为原型链中没有条目来获取基类方法。

this 参数

在 TypeScript 中,方法或函数定义中的名为 this 的初始参数具有特殊含义。这些参数在编译过程中被擦除:

type SomeType = any;
// ---cut---
// TypeScript 输入带有‘this’参数
function fn(this: SomeType, x: number) {/* ... */
}
// JavaScript 输出
function fn(x) {/* ... */
}

TypeScript 检查调用带有 this 参数的函数时,确保使用了正确的上下文。除了使用箭头函数外,我们还可以在方法定义中添加 this 参数,以静态地强制执行正确的方法调用:

// @errors: 2684
class MyClass {name = "MyClass";getName(this: MyClass) {return this.name;}
}
const c = new MyClass();
// 正确调用
c.getName();// 错误,会导致崩溃
const g = c.getName;
console.log(g());

这种方法具有与箭头函数方法相反的权衡:

  • JavaScript 调用者可能仍然在不知情的情况下错误地使用类方法。
  • 每个类定义只分配一个函数,而不是每个类实例一个函数。
  • 仍然可以通过 super 调用基本方法定义。

this 类型

在类中,一个特殊的类型 this 动态地指向当前类的类型。让我们看一下它的用法:

class Box {contents: string = "";set(value: string) {
//  ^?this.contents = value;return this;}
}

在这里,TypeScript 推断出 set 的返回类型是 this,而不是 Box。现在让我们创建 Box 的一个子类:

class Box {contents: string = "";set(value: string) {this.contents = value;return this;}
}
// ---cut---
class ClearableBox extends Box {clear() {this.contents = "";}
}const a = new ClearableBox();
const b = a.set("你好");
//    ^?

你还可以在参数类型注释中使用 this

class Box {content: string = "";sameAs(other: this) {return other.content === this.content;}
}

这与编写 other: Box 是不同的——如果你有一个派生类,它的 sameAs 方法现在只接受同一派生类的其他实例:

// @errors: 2345
class Box {content: string = "";sameAs(other: this) {return other.content === this.content;}
}class DerivedBox extends Box {otherContent: string = "?";
}const base = new Box();
const derived = new DerivedBox();
derived.sameAs(base);

基于 this 的类型护卫

在类和接口的方法中,你可以在返回位置使用 this is Type。当与类型缩小(例如 if 语句)混合使用时,目标对象的类型将缩小为指定的 Type

// @strictPropertyInitialization: false
class FileSystemObject {isFile(): this is FileRep {return this instanceof FileRep;}isDirectory(): this is Directory {return this instanceof Directory;}isNetworked(): this is Networked & this {return this.networked;}constructor(public path: string, private networked: boolean) {}
}class FileRep extends FileSystemObject {constructor(path: string, public content: string) {super(path, false);}
}class Directory extends FileSystemObject {children: FileSystemObject[];
}interface Networked {host: string;
}const fso: FileSystemObject = new FileRep("foo/bar.txt", "foo");if (fso.isFile()) {fso.content;
// ^?
} else if (fso.isDirectory()) {fso.children;
// ^?
} else if (fso.isNetworked()) {fso.host;
// ^?
}

基于 this 的类型护卫的常见用例是允许对特定字段进行延迟验证。例如,以下示例在 hasValue 被验证为 true 时,从 box 中删除了 undefined 值:

class Box<T> {value?: T;hasValue(): this is { value: T } {return this.value !== undefined;}
}const box = new Box();
box.value = "Gameboy";box.value;
//  ^?if (box.hasValue()) {box.value;//  ^?
}

参数属性

TypeScript 提供了一种特殊的语法,可以将构造函数的参数转换为具有相同名称和值的类属性。这些被称为参数属性,通过在构造函数参数前加上可见性修饰符 publicprivateprotectedreadonly 来创建。生成的字段将具有这些修饰符:

// @errors: 2341
class Params {constructor(public readonly x: number,protected y: number,private z: number) {// 不需要主体代码}
}
const a = new Params(1, 2, 3);
console.log(a.x);
//            ^?
console.log(a.z);

类表达式

背景阅读:
类表达式(MDN)

类表达式与类声明非常相似。唯一的真正区别是类表达式不需要名称,尽管我们可以通过将它们绑定到标识符来引用它们:

const someClass = class<Type> {content: Type;constructor(value: Type) {this.content = value;}
};const m = new someClass("你好,世界");
//    ^?

构造函数签名

JavaScript 类使用 new 运算符进行实例化。对于某个类本身的类型,InstanceType 类型模拟了这个操作。

class Point {createdAt: number;x: number;y: numberconstructor(x: number, y: number) {this.createdAt = Date.now()this.x = x;this.y = y;}
}
type PointInstance = InstanceType<typeof Point>function moveRight(point: PointInstance) {point.x += 5;
}const point = new Point(3, 4);
moveRight(point);
point.x; // => 8

abstract 类和成员

在 TypeScript 中,类、方法和字段可以是抽象的

抽象方法抽象字段是指没有提供实现的方法或字段。这些成员必须存在于抽象类中,抽象类不能直接被实例化。

抽象类的作用是作为子类的基类,子类需要实现所有的抽象成员。当一个类没有任何抽象成员时,它被称为具体类

让我们来看一个例子:

// @errors: 2511
abstract class Base {abstract getName(): string;printName() {console.log("你好," + this.getName());}
}const b = new Base();

我们不能使用 new 实例化 Base,因为它是抽象的。相反,我们需要创建派生类并实现抽象成员:

abstract class Base {abstract getName(): string;printName() {}
}
// ---cut---
class Derived extends Base {getName() {return "世界";}
}const d = new Derived();
d.printName();

请注意,如果我们忘记实现基类的抽象成员,将会遇到错误:

// @errors: 2515
abstract class Base {abstract getName(): string;printName() {}
}
// ---cut---
class Derived extends Base {// 忘记实现任何内容
}

抽象构造函数签名

有时候,你希望接受某个类构造函数,该构造函数生成的实例派生自某个抽象类。

例如,你可能希望编写以下代码:

// @errors: 2511
abstract class Base {abstract getName(): string;printName() {}
}
class Derived extends Base {getName() {return "";}
}
// ---cut---
function greet(ctor: typeof Base) {const instance = new ctor();instance.printName();
}

TypeScript 正确地告诉你,你正在尝试实例化一个抽象类。毕竟,根据 greet 的定义,编写以下代码是完全合法的,这将构造一个抽象类:

declare const greet: any, Base: any;
// ---cut---
// 错误!
greet(Base);

相反,你想编写一个接受具有构造函数签名的内容的函数:

// @errors: 2345
abstract class Base {abstract getName(): string;printName() {}
}
class Derived extends Base {getName() {return "";}
}
// ---cut---
function greet(ctor: new () => Base) {const instance = new ctor();instance.printName();
}
greet(Derived);
greet(Base);

现在 TypeScript 正确地告诉你哪些类构造函数可以调用——Derived 可以,因为它是具体类,但 Base 不能。

类之间的关系

在大多数情况下,TypeScript 中的类是按照结构进行比较的,与其他类型一样。

例如,这两个类可以互相替换使用,因为它们是相同的:

class Point1 {x = 0;y = 0;
}class Point2 {x = 0;y = 0;
}// 正常
const p: Point1 = new Point2();

类之间的子类型关系也存在,即使没有显式的继承:

// @strict: false
class Person {name: string;age: number;
}class Employee {name: string;age: number;salary: number;
}// 正常
const p: Person = new Employee();

这听起来很简单,但有一些情况会让人感到很奇怪。

空类没有成员。在结构类型系统中,没有成员的类型通常是其他任何类型的超类型。因此,如果你编写一个空类(不要这样做!),则可以使用任何类型来替代它:

class Empty {}function fn(x: Empty) {// 对于 'x' 无法执行任何操作,所以我们不会做任何事情
}// 全部都可以!
fn(window);
fn({});
fn(fn);

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/729842.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Aigtek前置微小信号放大器有什么作用

前置微小信号放大器是一种被广泛应用于无线通信、雷达、射频等领域中的低噪声放大器。相较于传统的放大器&#xff0c;前置微小信号放大器具有更高的灵敏度和更低的噪声系数。下面安泰Aigtek将介绍前置微小信号放大器的作用和意义。 一、前置微小信号放大器的作用 放大弱信号 前…

【golang】二叉树的遍历

本文使用golang实现二叉树的遍历&#xff0c;包含以下7种方法。 深度优先遍历 先序遍历 递归法非递归法 中序遍历 递归法非递归法 后序遍历 递归法非递归法 广度优先遍历 二叉树节点定义&#xff1a; type Node struct {Val intLeft *NodeRight *Node }深度优先遍历 先…

C语言实现回调函数

C语言实现回调函数 一、回调函数概念1.1 什么叫函数指针 二、回调函数案例 一、回调函数概念 回调函数就是一个被作为参数传递的函数。在C语言中&#xff0c;回调函数只能使用函数指针实现&#xff0c;在C、Python、ECMAScript等更现代的编程语言中还可以使用仿函数或匿名函数…

IDEA启动项目读取nacos乱码导致启动失败

新安装的2023社区版IDEA,启动项目报错。 forest: interceptors: - com.gdsz.b2b.frontend.api.Interceptors.ApiInterceptor org.yaml.snakeyaml.error.YAMLException: java.nio.charset.MalformedInputException: Input length 1 at org.yaml.snakeyaml.reader.S…

Java面试题总结10之MySQL索引和锁

索引的基本原理 把无需的数据变成有序的查询 1&#xff0c;把创建了索引的列的内容进行排序 2&#xff0c;对排序结果生成倒排表 3&#xff0c;到倒排表内容上拼上数据地址链 4&#xff0c;在查询的时候&#xff0c;先拿到倒排表内容&#xff0c;再取出数据地址链&#xf…

error: cannot bind non-const lvalue reference of type 解决方法

error: cannot bind non-const lvalue reference of type 解决方法 问题来源解决方法 问题来源 使用direct_visual_lidar_calibration时需要安装Iridescence 编译时出现如下错误 /home/flipper/iridescence/src/glk/glsl_shader.cpp:289:36: error: cannot bind non-const lv…

7-3 前世档案(Python)

网络世界中时常会遇到这类滑稽的算命小程序&#xff0c;实现原理很简单&#xff0c;随便设计几个问题&#xff0c;根据玩家对每个问题的回答选择一条判断树中的路径&#xff08;如下图所示&#xff09;&#xff0c;结论就是路径终点对应的那个结点。 现在我们把结论从左到右顺序…

基于Leatlet标注Geojson下载器实现

在上一篇文章中&#xff0c;我们学习了Leaflet的基础知识&#xff0c;包括如何创建地图、添加图层等。在本文中&#xff0c;我们将深入学习Leaflet中标注的创建和管理&#xff0c;包括如何添加标注、自定义标注图标、创建图层组、批量添加和删除标注、为标注添加属性和弹出框等…

手机卡丢失了怎么办?挂失与补办全攻略来了

现在人人都离不开手机&#xff0c;手机改变了我们的生活&#xff0c;但是有时我们可能会遇到手机卡丢失或被盗的情况。这时&#xff0c;及时挂失并补办新的手机卡至关重要&#xff0c;以确保个人信息安全和通信畅通。这里小编将为您提供详细的手机卡挂失与补办流程指南&#xf…

day38 动态规划part1

509. 斐波那契数 简单 斐波那契数 &#xff08;通常用 F(n) 表示&#xff09;形成的序列称为 斐波那契数列 。该数列由 0 和 1 开始&#xff0c;后面的每一项数字都是前面两项数字的和。也就是&#xff1a; F(0) 0&#xff0c;F(1) 1 F(n) F(n - 1) F(n - 2)&#xff0c;…

VS配置开发与远程调试笔记

先简单写一下&#xff0c;后续详细补充 场景&#xff1a;本地机器开发&#xff0c;虚拟机调试 准备工作&#xff1a; 由于要将生成的文件生成在虚拟机&#xff0c;避免反复拷贝&#xff0c;直接配置虚拟机共享文件夹进行写入&#xff0c;步骤如下&#xff1a; 虚拟机打开网…

Vue3:ref和reactive实现响应式数据

一、情景说明 在Vue2中&#xff0c;我们已经知道数据的响应式&#xff0c;是什么含义 就是&#xff0c;在data块中&#xff0c;定义的变量&#xff0c;在页面中引用后 任何地方修改了该变量&#xff0c;页面中引用的变量会立即显示最新数值。 这块&#xff0c;我们学习了 插值…

Leetcode : 1122. 数组的相对排序

思路&#xff1a;计数排序&#xff0c;然后根据计数的数组序列重构数组&#xff0c;针对多余的数组元素采取sort函数进行升序排列加在末尾即可。 #include <iostream> #include <vector> #include <algorithm>using namespace std;class Solution { public:v…

ChatGPT消息发不出去了?我找到解决方案了

现象 今天忽然发现 ChatGPT无法发送消息&#xff0c;能查看历史对话&#xff0c;但是无法发送消息。 猜测原因 出现这个问题的各位&#xff0c;应该都是点击登录后顶部弹窗邀请[加入多语言 alapha 测试]了&#xff0c;并且语言选择了中文&#xff0c;抓包看到ab.chatgpt.com…

win11修改主机mac地址

很多时候&#xff0c;为了限制恶意的蹭流浪&#xff0c;除了分配固定的ip地址外&#xff0c;还限制mac地址。只有mac与ip一致&#xff0c;才能上网冲浪 网络适配器中修改 搜索“控制面板”打开 控制面板 > 网络和Internet > 网络和共享中心 >查看网络状态和任务>…

【操作系统学习笔记】文件管理1.9

【操作系统学习笔记】文件管理1.9 参考书籍: 王道考研 视频地址: Bilibili 文件保护 口令保护 为文件设置一个口令&#xff0c;用户请问访问该文件时必须提供口令交由系统验证 优点 保存口令的空间开销小验证口令的时间开销小 缺点 正确的口令存放在系统内部&#xff0c;不…

kafka 可视化工具

kafka可视化工具 随着科技发展&#xff0c;中间件也百花齐放。平时我们用的redis&#xff0c;我就会通过redisInsight-v2 来查询数据&#xff0c;mysql就会使用goland-ide插件来查询&#xff0c;都挺方便。但是kafka可视化工具就找了半天&#xff0c;最后还是觉得redpandadata…

【框架】React和Vue的异同

1. 前言 React对于原生JS要求会高一级&#xff0c;国外React用的多&#xff0c;国内Vue用的多。 2. 共同点 组件化函数式编程 &#xff08;vue3函数式编程、vue2声明式编程&#xff09;单向数据流&#xff0c;数据驱动视图VirtualDOM Diff算法操作DOM社区成熟&#xff0c;…

在哪里能找到抖音短视频素材?推荐热门的抖音短视频素材下载资源

哎呦喂&#xff0c;小伙伴们&#xff0c;是不是在短视频的大海里划船&#xff0c;想找到那颗能让你起飞的珍珠&#xff0c;但又觉得素材难寻如针海捞针&#xff1f;别急&#xff0c;今天我就来给你们送上几个超实用的宝藏素材网站&#xff0c;让你的短视频创作不再愁素材 1&am…

Docker容器的操作

目录 运行容器 查看容器 查看容器详细信息 删除容器 启动容器 停止容器 重启容器 暂停容器 激活容器 杀死容器 进入容器 常用 查看容器的日志 拷贝容器的文件到本地 容器改名 查看容器资源 查看容器内部的进程 监测容器发生的事件 检测容器停止以后的反回值…