关于Class的学习

字段

strictPropertyInitialization
选项控制了类字段是否需要在构造函数里初始化

注意,字段需要在构造函数自身进行初始化。TypeScript 并不会分析构造函数里你调用的方法,进而判断初始化的值,因为一个派生类也许会覆盖这些方法并且初始化成员失败

如果不希望通过constructor初始化,可以加上非空断言

1
2
3
4
class OKGreeter {
// Not initialized, but no error
name!: string;
}

readonly

通过前缀readonly阻止在构造函数外赋值

构造函数

可以使用带类型注解的参数、默认值、重载等
但类构造函数签名与函数签名之间也有一些区别

  • 构造函数不能有类型参数
  • 构造函数不能有返回类型注解,因为总是返回类实例类型

Super 调用
ts会在需要的时候提醒调用super()

方法

方法跟函数、构造函数一样,使用相同的类型注解.TypeScript 并没有给方法添加任何新的东西

注意在一个方法体内,它依然可以通过 this. 访问字段和其他的方法。方法体内一个未限定的名称(unqualified name,没有明确限定作用域的名称)总是指向闭包作用域里的内容

Getters / Setter

TypeScript 对存取器有一些特殊的推断规则

  • 如果 get 存在而 set 不存在,属性会被自动设置为 readonly
  • 如果 setter 参数的类型没有指定,它会被推断为 getter 的返回类型
  • getters 和 setters 必须有相同的成员可见性(Member Visibility。

从 TypeScript 4.3 起,存取器在读取和设置的时候可以使用不同的类型

索引签名

类可以声明索引签名,它和对象类型的索引签名是一样的

类继承

implements
通过 implements 语句检查是否满足接口
类也可以实现多个接口,比如 class C implements A, B {

implements 语句仅仅检查类是否按照接口类型实现,但它并不会改变类的类型或者方法的类型

extend
类可以 extend 一个基类。一个派生类有基类所有的属性和方法,还可以定义额外的成员

一个派生类可以覆写一个基类的字段或属性。你可以使用 super 语法访问基类的方法

TypeScript 强制要求派生类总是它的基类的子类型

初始化顺序

类初始化的顺序,就像在 JavaScript 中定义的那样:

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

成员可见性

public
默认的可见性为 public,一个 public 的成员可以在任何地方被获取

protected
protected 成员仅仅对子类可见

暴露受保护成员

1
2
3
4
5
6
7
8
9
class Base {
protected m = 10;
}
class Derived extends Base {
// No modifier, so default is 'public'
m = 15;
}
const d = new Derived();
console.log(d.m); // OK

交叉等级受保护成员访问

不同的 OOP 语言在通过一个基类引用是否可以合法的获取一个 protected 成员是有争议的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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: Base) {
other.x = 10;
// Property 'x' is protected and only accessible through an instance of class 'Derived2'. This is an instance of class 'Base'.
}
}

java合法,ts不合法,C#和C++也如此

private

类似 protected ,但是不允许访问成员,即便是子类

交叉实例私有成员访问
TypeScript 允许交叉实例私有成员的获取:

1
2
3
4
5
6
7
8
class A {
private x = 10;

public sameAs(other: A) {
// No error
return other.x === this.x;
}
}

警告
private和 protected 仅仅在类型检查的时候才会强制生效.意味着在 JavaScript 运行时,像 in 或者简单的属性查找,依然可以获取 private 或者 protected 成员.

private 允许在类型检查的时候,通过方括号语法进行访问。这让比如单元测试的时候,会更容易访问 private 字段,这也让这些字段是弱私有(soft private)而不是严格的强制私有

静态成员

类可以有静态成员,静态成员跟类实例没有关系,可以通过类本身访问到

同样可以使用 public protected 和 private 这些可见性修饰符

静态成员也可以被继承

特殊静态名称

类本身是函数,而覆写 Function 原型上的属性通常认为是不安全的,因此不能使用一些固定的静态名称,函数属性像 name、length、call 不能被用来定义 static 成员

为什么没有静态类

静态类之所以存在是因为语言将数据限制在类里。但ts中不存在,所以没有必要。一个对象或者单独的类就能完成

类静态块

允许你写一系列有自己作用域的语句,也可以获取类里的私有字段

意味着我们可以安心的写初始化代码:正常书写语句,无变量泄漏,还可以完全获取类中的属性和方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Foo {
static #count = 0;

get count() {
return Foo.#count;
}

static {
try {
const lastInstances = loadLastInstances();
Foo.#count += lastInstances.length;
}
catch {}
}
}

泛型类

累得类型参数的推断和函数调用时同样的方式

类跟接口一样也可以使用泛型约束以及默认值

1
2
3
4
5
6
7
8
9
10
class Box<Type> {
contents: Type;
constructor(value: Type) {
this.contents = value;
}
}

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

静态成员中的类型参数

1
2
3
4
5
class Box<Type> {
static defaultValue: Type;
// Static members cannot reference class type parameters.
}

类型会被完全抹除,运行时,只有一个 Box.defaultValue 属性槽。这也意味着如果设置 Box<string>.defaultValue 是可以的话,这也会改变 Box<number>.defaultValue

类运行时的 this

ts提供了一些方式去减缓或阻止js本身this指向比较混乱的问题

箭头函数

如果你有一个函数,经常在被调用的时候丢失 this 上下文,使用一个箭头函数或许更好些

1
2
3
4
5
6
7
8
9
10
11
class MyClass {
name = "MyClass";
getName = () => {
return this.name;
};
}
const c = new MyClass();
const g = c.getName;
// Prints "MyClass" instead of crashing
console.log(g());

  • this 的值在运行时是正确的,即使 TypeScript 不检查代码
  • 这会使用更多的内存,因为每一个类实例都会拷贝一遍这个函数。
  • 你不能在派生类使用 super.getName ,因为在原型链中并没有入口可以获取基类方法。

this 参数

在 TypeScript 方法或者函数的定义中,第一个参数且名字为 this 有特殊的含义。该参数会在编译的时候被抹除

1
2
3
4
5
// TypeScript input with 'this' parameter
function fn(this: SomeType, x: number) {
/* ... */
}

1
2
3
4
// JavaScript output
function fn(x) {
/* ... */
}
  • JavaScript 调用者依然可能在没有意识到它的时候错误使用类方法
  • 每个类一个函数,而不是每一个类实例一个函数
  • 基类方法定义依然可以通过 super 调用

this 类型

在类中,有一个特殊的名为 this 的类型,会动态的引用当前类的类型

1
2
3
4
5
6
7
8
class Box {
contents: string = "";
set(value: string) {
// (method) Box.set(value: string): this
this.contents = value;
return this;
}
}

这里,TypeScript 推断 set 的返回类型为 this 而不是 Box

也可以在参数类型注解中使用 this

1
2
3
4
5
6
class Box {
content: string = "";
sameAs(other: this) {
return other.content === this.content;
}
}

如果你有一个派生类,它的 sameAs 方法只接受来自同一个派生类的实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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);
// Argument of type 'Box' is not assignable to parameter of type 'DerivedBox'.
// Property 'otherContent' is missing in type 'Box' but required in type 'DerivedBox'.


基于 this 的类型保护

可以在类和接口的方法返回的位置,使用 this is Type 。当搭配使用类型收窄 (举个例子,if 语句),目标对象的类型会被收窄为更具体的 Type

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Box<T> {
value?: T;

hasValue(): this is { value: T } {
return this.value !== undefined;
}
}

const box = new Box();
box.value = "Gameboy";

box.value;
// (property) Box<unknown>.value?: unknown

if (box.hasValue()) {
box.value;
// (property) value: unknown
}

参数属性

TypeScript 提供了特殊的语法,可以把一个构造函数参数转成一个同名同值的类属性。这些就被称为参数属性.可以通过在构造函数参数前添加一个可见性修饰符 public private protected 或者 readonly 来创建参数属性,最后这些类属性字段也会得到这些修饰符

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Params {
constructor(
public readonly x: number,
protected y: number,
private z: number
) {
// No body necessary
}
}
const a = new Params(1, 2, 3);
console.log(a.x);
// (property) Params.x: number

console.log(a.z);
// Property 'z' is private and only accessible within class 'Params'.

类表达式

类表达式跟类声明非常类似,唯一不同的是类表达式不需要一个名字,尽管我们可以通过绑定的标识符进行引用

1
2
3
4
5
6
7
8
9
const someClass = class<Type> {
content: Type;
constructor(value: Type) {
this.content = value;
}
};

const m = new someClass("Hello, world");
// const m: someClass<string>

抽象类和成员

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

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

抽象类的作用是作为子类的基类,让子类实现所有的抽象成员。当一个类没有任何抽象成员,他就会被认为是具体的(concrete)

抽象构造签名

有的时候,你希望接受传入可以继承一些抽象类产生一个类的实例的类构造函数

1
2
3
4
5
6
7
8
9
10
11
12
13
abstract class Base {
abstract getName(): string;

printName() {
console.log("Hello, " + this.getName());
}
}

function greet(ctor: typeof Base) {
const instance = new ctor();
// Cannot create an instance of an abstract class.
instance.printName();
}

但如果你写一个函数接受传入一个构造签名

1
2
3
4
5
6
7
8
9
10
function greet(ctor: new () => Base) {
const instance = new ctor();
instance.printName();
}
greet(Derived);
greet(Base);

// Argument of type 'typeof Base' is not assignable to parameter of type 'new () => Base'.
// Cannot assign an abstract constructor type to a non-abstract constructor type.

现在 TypeScript 会正确的告诉你,哪一个类构造函数可以被调用,Derived 可以,因为它是具体的,而 Base 是不能的。

作者

徐云飞

发布于

2022-10-27

更新于

2023-02-05

许可协议

评论