教程中的难点3
提示
本文整理自 TypeScript
官方文档中的 handlebook,版本为 V5.4
# 类
# 类的组成部分
- 类的字段
- 字段声明可以在类上创建公共可写属性
- 可以声明类型,不声明则隐式为any
- 类的字段在声明时可以给定初始值
- 设置
strictPropertyInitialization
为true
则必须给字段初始值 - 如果不希望初始化,但是又设置了
strictPropertyInitialization
,可以使用非空断言 - 在字段面前加入
readonly
则该字段不能被修改
class Person {
name: string
age // 隐式为any
gender!: 'male' | 'female' // 设置非空断言时不初始化也不会报错
readonly privateHobby: string // 设置了 readonly 后无法修改
}
2
3
4
5
6
- 类的构造函数
- 类的构造函数也可以加入带有注释、默认值、重载的参数
- 类的构造函数不能有类型参数,即无法使用泛型
- 类的构造函数不能设置返回值的类型
- 调用
super()
必须要在使用this
之前
- 类的方法
- 除了类型标注意外,ts的类方法与js无异
- 类的 getter 和 setter
- 对访问器有一些限制
- get存在而set不存在时默认只读
- 未指定setter的类型,将会从getter的返回类型进行推断
- getter和setter必须有相同的成员可见性
- ts 4.3 开始,getter和setter可以设置不同的参数类型
class Thing {
_size = 0;
get size(): number {
return this._size;
}
set size(value: string | number | boolean) {
let num = Number(value);
// Don't allow NaN, Infinity, etc
if (!Number.isFinite(num)) {
this._size = 0;
return;
}
this._size = num;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
- 索引签名,与对象的索引签名类似
class MyClass {
[s: string]: boolean | ((s: string) => boolean);
check(s: string) {
return this[s] as boolean;
}
}
2
3
4
5
6
7
注意
由于索引签名类型还需要捕获方法的类型,因此要有效地使用这些类型并不容易。通常,最好将索引数据存储在另一个位置,而不是存储在类实例本身上。
# 类的继承
implements
子句用以判断class
是否实现了interface
中的所有成员implements
实现多个interface
,中间使用逗号隔开- 值得注意的时,
implements
子句不会完善class
的类型,一切都还需要自己重新声明
- 值得注意的时,
extends
- 当使用
extends
关键字时,派生类会继承基类所有的属性与方法,同时还可以定义其他成员 - 派生类可以重写基类的方法,如果需要使用基类方法,可以使用
super.method()
的写法
- 当使用
值得注意的一点是,派生类在重写基类方法时,一定要遵循基类的规范,因为在ts的背景下,会使用基类来引用派生类的实例
class Base {
greet() {
console.log("Hello, world!");
}
}
class Derived extends Base {
greet(name?: string) {
if (name === undefined) {
super.greet();
} else {
console.log(`Hello, ${name.toUpperCase()}`);
}
}
}
const d = new Derived();
d.greet();
d.greet("reader");
// Alias the derived instance through a base class reference
const b: Base = d;
// No problem
b.greet();
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
注意
当 target >= ES2022 或 useDefineForClassFields
为 true
时,类字段将在父类构造函数完成后初始化,覆盖父类设置的任何值。当您只想为继承的字段重新声明更准确的类型时,这可能是一个问题。若要处理这些情况,可以编写 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 {
// Does not emit JavaScript code,
// only ensures the types are correct
declare resident: Dog;
constructor(dog: Dog) {
super(dog);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
JavaScript
类的初始化顺序如下:- 基类字段初始化 -> 基类
constructor
运行 -> 派生类字段初始化 -> 派生类constructor
运行 - 基类
constructor
运行时已经看到了自己的字段,但是还没有看到派生类的字段
- 基类字段初始化 -> 基类
提示
extends
有一定的历史兼容性问题
在 ES2015 中,返回对象的构造函数隐式地将 this 的值替换为 super(...) 的任何调用方。生成的构造函数代码必须捕获 super(...) 的任何潜在返回值并将其替换为 this 。
因此,子类化 Error 、 Array 和其他 可能不再按预期工作。这是因为 Error , Array 等的构造函数使用 ECMAScript 6 的 new.target
来调整原型链;但是,在 ECMAScript 5 中调用构造函数时,无法确保 new.target 的值。默认情况下,其他下层编译器通常具有相同的限制。
建议在任何 super(...) 调用后立即手动调整原型。
class MsgError extends Error {
constructor(m: string) {
super(m);
// Set the prototype explicitly.
Object.setPrototypeOf(this, MsgError.prototype);
}
sayHello() {
return "hello " + this.message;
}
}
2
3
4
5
6
7
8
9
10
11
12
但是, MsgError 的任何子类也必须手动设置原型。对于不支持 Object.setPrototypeOf 的运行时,您可以改用 __proto__
遗憾的是,这些解决方法不适用于 Internet Explorer 10 及更早版本。可以手动将方法从原型复制到实例本身(即 MsgError.prototype 到 this ),但原型链本身无法固定。
# 成员可见性
# pulblic
类成员的默认可见性为 public
,可以在类的任何位置上访问类成员
# protected
protected
成员仅对类本身及继承类的派生类可见
class Greeter {
public greet() {
console.log("Hello, " + this.getName());
}
protected getName() {
return "hi";
}
}
class SpecialGreeter extends Greeter {
public howdy() {
// OK to access protected member here
console.log("Howdy, " + this.getName());
}
}
const g = new SpecialGreeter();
g.greet(); // OK
g.getName();
// Property 'getName' is protected and only accessible within class 'Greeter' and its subclasses.
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
- 派生类可以将基类的
protected
属性变为public
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
2
3
4
5
6
7
8
9
如果要保证 m 仍然为
protected
的,需要加入修饰符,否则会默认为public
不同派生类之间无法通过基类来访问
protected
可见性的成员
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) {
// Property 'x' is protected and only accessible within class 'Derived1' and its subclasses.
other.x = 10;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# private
仅允许类本身访问其
private
可见性的成员ts 允许跨实例访问
private
成员
class A {
private x = 10;
public sameAs(other: A) {
// No error
return other.x === this.x;
}
}
2
3
4
5
6
7
8
注意
与 TypeScript 类型系统的其他方面一样, private 和 protected 仅在类型检查期间强制执行。
这意味着 JavaScript 运行时构造(如 in 或简单属性查找)仍然可以访问 private 或 protected 成员
private 还允许在类型检查期间使用括号表示法进行访问。这使得 private 声明的字段可能更容易访问单元测试等内容,缺点是这些字段是软私有的,并且不严格强制执行隐私。
与 TypeScripts 的 private 不同,JavaScript 的私有字段 ( # ) 在编译后保持私有,并且不提供前面提到的转义舱口,如括号符号访问,使它们成为硬私有的。
编译到 ES2021 或更低版本时,TypeScript 将使用 WeakMaps 代替 # 。
如果需要保护类中的值免受恶意执行者的侵害,则应使用提供硬运行时隐私的机制,例如闭包、WeakMaps 或私有字段。请注意,在运行时添加的这些隐私检查可能会影响性能。
# 静态成员
静态成员不与类的特定实例相绑定,而只能通过类的构造函数对象本身去访问
class MyClass {
static x = 0;
static printX() {
console.log(MyClass.x);
}
}
console.log(MyClass.x);
MyClass.printX();
2
3
4
5
6
7
8
- 静态成员也可以使用
public
protected
private
来修饰 - 静态成员是可以继承的
- 特殊的静态成员。函数的属性,例如
name
length
call
是无法被设置为静态成员的 - TS中不需要静态的类,因为只有一个实例的类对应着一个对象
# 静态代码块
在静态代码块中,我们可以访问到 private
的属性并可以读写对应的值,这可以帮助我们初始化,完全访问内部变量而不会造成变量泄漏
class Foo {
static #count = 0;
get count() {
return Foo.#count;
}
static {
try {
const lastInstances = loadLastInstances();
Foo.#count += lastInstances.length;
}
catch {}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 泛型类
- 可以通过在类中加入泛型来控制类中某些成员的类型,泛型可以使用约束和接口来书写
class Box<Type> {
contents: Type;
constructor(value: Type) {
this.contents = value;
}
}
const b = new Box("hello!");
2
3
4
5
6
7
8
- 静态成员不可以加入泛型,因为静态成员是所有类共用的,不是每个实例私有的
# 类与 this
js
中的this
取决于如何被调用,当被直接调用时,访问的是类作为this
,如果被实例调用时,则访问的是实例中的属性
class MyClass {
name = "MyClass";
getName() {
return this.name;
}
}
const c = new MyClass();
const obj = {
name: "obj",
getName: c.getName,
};
// Prints "obj", not "MyClass"
console.log(obj.getName());
2
3
4
5
6
7
8
9
10
11
12
13
14
使用箭头函数可以修正上述问题,因为箭头函数没有自己的
this
,因此总会使用类中的this
,但是也有一些缺陷- 箭头函数不会被绑定在类的
prototype
上,因此,每个实例都会创建自己的箭头函数,从而占用更多的内存 - 箭头函数无法被继承,只能通过
super.method
来进行访问
- 箭头函数不会被绑定在类的
也可以通过为函数的
this
绑定特定的类型来进行限制,确保函数被正确访问
class MyClass {
name = "MyClass";
getName(this: MyClass) {
return this.name;
}
}
const c = new MyClass();
// OK
c.getName();
// Error, would crash
const g = c.getName;
console.log(g());
2
3
4
5
6
7
8
9
10
11
12
13
# this
的类型
this
总指向调用他的类
class Box {
content: string = ''
set (val: string) {
this.content = val
return this
}
}
class ClearableBox extends Box {
clear () {
this.content = ''
}
}
const clearableBox = new ClearableBox()
const anotherClearableBox = clearableBox.set('hello') // anotherClearableBox 的类型将会为 ClearableBox
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
- 如果通过参数闭包了类的 this,将无法传递其他的类
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'.
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 基于 this
的类型守卫
- 使用类型谓语做类型缩窄
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;
// const fso: FileRep
} else if (fso.isDirectory()) {
fso.children;
// const fso: Directory
} else if (fso.isNetworked()) {
fso.host;
// const fso: Networked & FileSystemObject
}
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
28
29
30
31
32
33
34
35
36
37
38
39
- 通常这种方式被用来限定不为
undefined
class Box<T> {
value?: T;
hasValue(): this is { value: T } {
return this.value !== undefined;
}
}
const box = new Box<string>();
box.value = "Gameboy";
box.value;
// (property) Box<string>.value?: string
if (box.hasValue()) {
box.value;
// (property) value: string
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 参数修饰符
- 可以为参数添加
public
protected
private
等修饰符
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'.
2
3
4
5
6
7
8
9
10
11
12
13
14
# 类表达式
类表达式与类声明非常相似。唯一真正的区别是类表达式不需要名称,尽管我们可以通过它们最终绑定到的任何标识符来引用它们
const someClass = class<Type> {
content: Type;
constructor(value: Type) {
this.content = value;
}
};
const m = new someClass("Hello, world");
// const m: someClass<string>
2
3
4
5
6
7
8
9
# 构造函数签名
JavaScript 类使用 new 运算符实例化。给定类本身的类型,InstanceType 实用工具类型将对此操作进行建模。
class Point {
createdAt: number;
x: number;
y: number
constructor(x: number, y: number) {
this.createdAt = Date.now()
this.x = x;
this.y = y;
}
}
type PointInstance = InstanceType<typeof Point> // 从这里获取到了 class 类的 实例的类型
function moveRight(point: PointInstance) {
point.x += 5;
}
const point = new Point(3, 4);
moveRight(point);
point.x; // => 8
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 抽象类和抽象成员
TypeScript 中的类、方法和字段可能是抽象的。
抽象方法或抽象字段是尚未提供实现的方法或抽象字段。这些成员必须存在于抽象类中,不能直接实例化。
抽象类的作用是充当子类的基类,这些子类确实实现了所有抽象成员。当一个类没有任何抽象成员时,它被称为具体的。
abstract class Base {
abstract getName(): string;
printName() {
console.log("Hello, " + this.getName());
}
}
const b = new Base();
// Cannot create an instance of an abstract class.
2
3
4
5
6
7
8
9
10
我们不能用 new 实例化 Base ,因为它是抽象的。相反,我们需要创建一个派生类并实现抽象成员:
class Derived extends Base {
getName() {
return "world";
}
}
const d = new Derived();
d.printName();
2
3
4
5
6
7
8
# 从抽象类派生类中创造实例
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.
2
3
4
5
6
7
8
# 类之间的关系
当类的结构相同时,被认为是同一个类
即使类之间没有显式继承,类之间仍然会存在父子关系
class Person {
name: string;
age: number;
}
class Employee {
name: string;
age: number;
salary: number;
}
// OK
const p: Person = new Employee();
2
3
4
5
6
7
8
9
10
11
12
13
提示
空类没有成员,可以被任何其他类所替代,但是同样地,空类也没法做任何事情,不建议使用。