【TypeScript】深入学习TypeScript类(下)

yongxinz

关注

阅读 91

2022-08-16


🍂 前言

最近博主一直在创作​​TypeScript​​​的内容,所有的​​TypeScript​​​文章都在我的​​TypeScript从入门到精通​​专栏里,每一篇文章都是精心打磨的优质好文,并且非常的全面和细致,期待你的订阅❤️

本篇文章将继续去讲解​​TypeScript​​​中的​​class​​类(由于内容较多,将其分为了上下两篇,​​),这也许会是你看过的最全面最细致的​​TypeScript​​教程🚀🚀🚀!

文章目录

  • ​​🍂 前言​​
  • ​​1、泛型类​​
  • ​​2、this指向​​
  • ​​🍂 箭头函数​​
  • ​​🍂 this参数​​
  • ​​3、this类型​​
  • ​​4、基于类型守卫的this​​
  • ​​5、类表达式​​
  • ​​6、抽象类和成员​​
  • ​​🍂 抽象构造签名​​
  • ​​7、类之间的关系​​
  • ​​🍂 结语​​

1、泛型类

类和接口一样,可以是泛型的,当一个泛型类用​​new​​实例化时,其类型参数的推断方式与函数调用的方式相同:

class Box<Type> {
contents: Type;
constructor(value: Type) {
this.contents = value;
}
}
// const b: Box<string>
const b = new Box("hello!");
// 等同于const b = new Box<string>("hello!");

  • 泛型类的静态成员不能引用类型参数:
  • 【TypeScript】深入学习TypeScript类(下)_javascript

2、this指向

在​​JavaScript​​​中​​this​​​指向是一个头疼的问题,默认情况下函数内​​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());

​TypeScript​​提供了一些方法来减少或防止这种错误:

🍂 箭头函数

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

使用箭头函数也是有一些妥协的:

  • ​this​​​ 值保证在运行时是正确的,即使是没有经过​​TypeScript​​检查的代码也是如此
  • 这将使用更多的内存,因为每个类实例将有它自己的副本,每个函数都是这样定义的
  • 你不能在派生类中使用​​super​​ 调用基类方法,因为在原型链中没有入口可以获取基类方法:

class MyClass {
name = "MyClass";
getName = () => {
return this.name;
};
}
class A extends MyClass {
AName: string;
constructor() {
super();
// getName为箭头函数时,调用super.getName()会报错
// this.AName = super.getName();
this.AName = this.getName(); // 但一直能通过this.getName()调用
}
}
const a = new A();
console.log(a.AName); // MyClass

🍂 this参数

在​​【TypeScript】深入学习TypeScript函数​​​中我们提到过​​this​​​参数,​​TypeScript​​​检查调用带有​​this​​ 参数的函数,是否在正确的上下文中进行

我们可以不使用箭头函数,而是在方法定义中添加一个​​this​​ 参数,以静态地确保方法被正确调用:

class MyClass {
name = "MyClass";
getName(this: MyClass) {
return this.name;
}
}
const c = new MyClass();
// 正确
c.getName();
// 错误
const g = c.getName;
console.log(g());

【TypeScript】深入学习TypeScript类(下)_typescript_02

这种方法做出了与箭头函数方法相反的取舍:

  • ​JavaScript​​调用者仍然可能在不知不觉中错误地使用类方法,如上面的例子:

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

  • 每个类定义只有一个函数被分配,而不是每个类实例一个函数
  • 基类方法定义仍然可以通过​​super​​ 调用。

3、this类型

在类中,一个叫做 ​​this​​ 的特殊类型动态地指向当前类的类型,看下面的这个例子:

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

在这里,​​TypeScript​​​推断出 ​​set​​​ 方法的返回类型是​​this​​​ ,而不是​​Box​​:

【TypeScript】深入学习TypeScript类(下)_派生类_03


创建​​Box​​的一个子类:

class ClearableBox extends Box {
clear() {
this.contents = "";
}
}
const a = new ClearableBox(); // a类型为ClearableBox
const b = a.set("hello"); // b类型为ClearableBox
console.log(b);

这里可以看到​​b​​​的类型竟然是​​ClearableBox​​​,这说明此时​​set​​​方法返回的​​this​​​类型指向了当前的类​​ClearableBox​​​(因为是在​​ClearableBox​​​上调用的​​set​​)

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

class Box {
contents: string = "";
// 类型注释中使用this
sameAs(other: this) {
return other.contents === this.contents;
}
}
class ClearableBox extends Box {
contents: string = "Ailjx";
}
class B {
contents: string = "";
}

const box = new Box();
const clearableBox = new ClearableBox();
const b = new B();

console.log(clearableBox.sameAs(box)); // false

// ❌❌❌报错
// 类型“B”的参数不能赋给类型“ClearableBox”的参数
// 类型 "B" 中缺少属性 "sameAs",但类型 "ClearableBox" 中需要该属性
console.log(clearableBox.sameAs(b));

上面例子中可以看到派生类​​ClearableBox​​​ 的​​sameAs​​ 方法能够接收基类的实例

但是当派生类中有额外的属性后,它就只能接收该同一派生类的其它实例了:

class Box {
contents: string = "";
sameAs(other: this) {
return other.contents === this.contents;
}
}
class ClearableBox extends Box {
otherContents: string = "Ailjx";
}

const box = new Box();
const clearableBox = new ClearableBox();

// ❌❌❌报错:
// 类型“Box”的参数不能赋给类型“ClearableBox”的参数。
// 类型 "Box" 中缺少属性 "otherContents",但类型 "ClearableBox" 中需要该属性。
console.log(clearableBox.sameAs(box));

4、基于类型守卫的this

我们可以在类和接口的方法的返回位置使用类型谓词​this is Type​​​,当与类型缩小混合时(例如​​if​​​语句),目标对象的的类型将被缩小到指定的​​Type​

类型谓词详见​​【TypeScript】TypeScript中类型缩小(含类型保护)与类型谓词​​

class Box {
// 利用类型谓词,当this类型是A的实例时,确保将this类型缩小为A类型
isA(): this is A {
return this instanceof A;
}
isB(): this is B {
return this instanceof B;
}
}

class A extends Box {
Apath: string = "A";
}
class B extends Box {
Bpath: string = "B";
}

// fso的类型为基类Box,它可能是A,也可能是B
const fso: Box = Math.random() > 0.5 ? new A() : new B();

if (fso.isA()) {
// fso.isA()为true时(说明Box的this类型指向了A,即可知道此时fso具体为A),
// 其通过类型谓词将fso缩小为了A类型,此时就可以安全调用A特有的属性
console.log(fso.Apath);
} else if (fso.isB()) {
console.log(fso.Bpath);
}

配合接口使用:

class Box {
isNetworked(): this is Networked & this {
return this.networked;
}
// networked属性控制Box是否包含Networked接口类型
constructor(private networked: boolean) {} // 这里使用了在构造器参数列表中声明属性
}
interface Networked {
host: string;
}

const A: Box = new Box(true);

// A.host = "12"; // ❌❌外界直接使用host属性报错:类型“Box”上不存在属性“host”

if (A.isNetworked()) {
// 此时A类型变成了Networked & this,可以安全使用host属性了
A.host = "12";
console.log(A.host); // 12
}

基于 ​​this​​​ 的类型保护的一个常见用例,是允许对一个特定字段进行懒惰验证。例如,这种情况下,当​​hasValue​​​ 被验证为真时,​​Box​​​类型缩小,​​value​​属性失去了可选性,就能直接使用了:

class Box<T> {
value?: T;
// 根据value值是否存在来缩小类型
hasValue(): this is { value: T } {
return this.value !== undefined;
}
}

const box = new Box<string>();

// value可能未定义需要使用可选连?
console.log(box.value?.toUpperCase());

if (box.hasValue()) {
// 这时Box类型已经缩小为{value:string}了,value不再是可选属性了,可以不使用可选连?了
console.log(box.value.toUpperCase());
}

5、类表达式

类表达式与类声明非常相似,唯一真正的区别是,类表达式不需要一个名字,我们可以通过它们最终绑定的任何标识符来引用它们:

const someClass = class<Type> {
content: Type;
constructor(value: Type) {
this.content = value;
}
};
// type m=someClass<string>
const m = new someClass("Hello, world");

6、抽象类和成员

使用​​abstract​​ 定义的一个方法或字段称为抽象成员,它是一个没有提供实现的方法或字段,这些成员必须存在于一个使用​​abstract​​ 定义的抽象类中,该类不能直接实例化

abstract class Base {
abstract getName(): string;
printName() {
console.log("Hello, " + this.getName());
}
}
// ❌❌❌报错:无法创建抽象类的实例
const b = new Base();

抽象类的作用是作为子类的基类实现所有的抽象成员

// 创建一个派生类实现抽象成员
class Derived extends Base {
getName() {
return "world";
}
}
const d = new Derived();
d.printName(); // Hello world

如果抽象类的派生类不实现它的抽象成员则会报错:

【TypeScript】深入学习TypeScript类(下)_抽象类_04

🍂 抽象构造签名

向上面这个例子,如果你想要写一个函数,能够接受所有抽象类​​Base​​的派生类,你可能会这样写:

function greet(ctor: typeof Base) {
const instance = new ctor();
instance.printName();
}

这时​​TypeScript​​会告诉你这样写是不对的:

【TypeScript】深入学习TypeScript类(下)_typescript_05


正确的做法应该是使用抽象构造签名

function greet(ctor: new () => Base) {
const instance = new ctor();
instance.printName();
}

完整示例:

abstract class Base {
abstract getName(): string;
printName() {
console.log("Hello, " + this.getName());
}
}

class Derived extends Base {
getName() {
return "world";
}
}
class Derived2 extends Base {
getName() {
return "world2";
}
}

function greet(ctor: new () => Base) {
const instance = new ctor();
instance.printName();
}

greet(Derived);
greet(Derived2);

// ❌❌❌报错:类型“typeof Base”的参数不能赋给类型“new () => Base”的参数。
// 无法将抽象构造函数类型分配给非抽象构造函数类型。
greet(Base);

7、类之间的关系

  • 相同的类可以互相替代使用:

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

  • 即使没有明确的继承,类之间的子类型关系也是存在的:

class Person {
name: string = "A";
age: number = 1;
}
class Employee {
name: string = "A";
age: number = 1;
salary: number = 99;
}
// type A = number
type A = Employee extends Person ? number : string;
// 正确
const p: Person = new Employee();

  • 空的类通常是其他任何东西的基类:

class Person {
name: string = "A";
age: number = 1;
}
class Employee {
salary: number = 99;
}
class N {}
// type A = number
type A = Person extends N ? number : string;
// type B = number
type B = Employee extends N ? number : string;

function fn(x: N) {}
// 以下调用均可
fn(Person);
fn(Employee);
fn(window);
fn({});
fn(fn);

🍂 结语

至此,​​TypeScript​​类的内容就全部结束了,关注博主下篇更精彩!

博主的​​TypeScript从入门到精通专栏​​正在慢慢的补充之中,赶快关注订阅,与博主一起进步吧!期待你的三连支持。

参考资料:​​TypeScript官网​​



精彩评论(0)

0 0 举报