大家好,我是半虹,这篇文章来讲 TypeScript 中的类
类是面向对象编程中的重要概念,可以提供一种组织和规划代码的方式
同时也是对象的蓝图,定义对象的状态和行为,也即对象的属性和方法
下面具体介绍 TypeScript 中类的用法,包括类的定义、使用、拓展等
1、定义
使用 class
关键字可以定义类,在类中可以声明属性和方法
(1)声明属性
可以在类的顶层声明属性,同时还能显式声明属性类型
若无显式声明,那么就会被隐式推导为 any
class Person {
// 声明属性
name: string; // 属性名为 name,类型为 string
age : number; // 属性名为 age ,类型为 number
}
另外还能设置属性默认值,此时属性类型声明可以省略
编译器会根据属性默认值来自动推导类型
class Person {
// 声明属性
name = 'Jack'; // 属性名为 name,默认值为 Jack,此时类型会被推导为 string
age = 18; // 属性名为 age ,默认值为 18 ,此时类型会被推导为 number
}
(2)声明方法
同样在类的顶层声明方法,此时可以在函数字面量声明参数和返回值类型
函数内可以使用 this
访问类中其它的属性和方法
class Person {
name: string;
age : number;
// 声明方法
// 接收零个参数,返回一个字符串
sayHi():string {
return `My name is ${this.name}. I am ${this.age} years old.`
}
}
(3)构造函数
构造函数在实例化对象时自动执行并返回一个对象,构造函数名称必须为 constructor
通常会在该函数内执行一些必要的初始化工作,例如初始化属性值
class Person {
name: string;
age : number;
// 构造函数
// 接收一个字符串和一个数字,分别用于初始化 name 和 age 属性,默认返回 Person 类型对象
constructor(name:string, age:number) {
this.name = name;
this.age = age ;
}
sayHi():string {
return `My name is ${this.name}. I am ${this.age} years old.`
}
}
(4)只读属性
可以使用 readonly
关键字声明某个属性只读,表示该属性无法被修改
只读属性 只能在声明时赋予初始值,或是在构造函数中赋予或修改其值
class Person {
readonly name: string = 'Jack'; // 声明只读属性,同时赋予初始值
readonly age : number; // 声明只读属性,没有赋予初始值
// 构造函数
constructor() {
this.name = 'Jacky'; // 修改只读属性,编译正常
this.age = 18; // 赋值只读属性,编译正常
}
// 普通方法
changeProps() {
this.name = 'Jacky'; // 修改只读属性,编译错误
this.age = 18; // 修改只读属性,编译错误
}
}
(5)存取器方法
存取器是特殊的类方法,用于控制对成员的访问,具体来说包括有两类:
- 取值器方法:使用
get
关键字定义,在属性被读取时调用,用于控制属性读取行为 - 存值器方法:使用
set
关键字定义,在属性被写入时调用,用于控制属性写入行为
class Person {
_name = 'Jack';
// 取值器方法,其中 get 是关键字,name 是属性名,在读取 name 属性时,该方法会自动调用
// 该方法不接收任何参数,且返回值类型就是属性类型
get name() {
return this._name;
}
// 存值器方法,其中 set 是关键字,name 是属性名,在写入 name 属性时,该方法会自动调用
// 该方法只接收一个参数,且参数的类型默认是取值器返回类型
set name(newName) {
this._name = newName;
}
}
注意,一个属性的存取器并不要求同时都有声明,此时会有以下的表现:
- 如果一个属性只声明取值器,不声明存值器,那么写入该属性时会失败
- 如果一个属性只声明存值器,不声明取值器,那么读取该属性时会返回
undefined
class Person {
_name = 'Jack';
_age = 18;
get name() { // 只有取值器
return this._name;
}
set age(newAge) { // 只有存值器
this._age = newAge;
}
getProps() {
console.log(this.name); // 正常
console.log(this.age); // 编译时正常,运行时返回 undefined
}
setProps() {
this.name = 'Jacky'; // 编译时报错,运行时不会执行任何逻辑
this.age = 21; // 正常
}
}
2、使用
(1)实例化
使用 new
关键字可以实例化对象,每个对象都有类中定义的属性和方法
需要注意的是,不同的实例化对象中属性和方法是独立的,它们互不影响
// 定义类
class Person {
// 声明属性
name = 'Jack';
// 声明方法
sayHi() {
console.log('Hi');
}
}
// 实例化对象
let person1 = new Person(); // person1 变量的类型是 Person,可以显式指定为 person1:Person
let person2 = new Person(); // person2 变量的类型是 Person,可以显式指定为 person2:Person
person1.name = 'Jacky';
console.log(person1.name); // Jacky
console.log(person2.name); // Jack
person1.sayHi = function() {
console.log('Hello');
}
person1.sayHi(); // Hello
person2.sayHi(); // Hi
(2)类型
TypeScript 中代码分为两种,分别是值代码和类型代码,二者位于不同的命名空间
一般情况下,代码要么属于值代码,要么属于类型代码,而在编译时只保留值代码
// 值代码
// 编译时会被保留
let a = 'string';
let b = function() { console.log('function'); }
// 类型代码
// 编译时会被删除
type c = number|string;
interface d {
e: number;
f: string;
}
但类声明比较特别,就和枚举一样,其既是类型也是值
编译器会根据上下文自动推断这时候用的是类型还是值,这种特性用起来十分方便
// 枚举
enum G {
h,
i,
j,
}
let g:G = G.h; // 其中,第一个 G 表示的是类型,第二个 G 表示的是值
// 类
class K {}
let k:K = new K(); // 其中,第一个 K 表示的是类型,第二个 K 表示的是值
总结一下,类本身就是类型,不过此时表示的是该类实例对象的类型
与此同时,类本身也是值,而这时可以使用 typeof
获取该类作为值时的类型
class Point {
x:number;
y:number;
// 构造函数
constructor(x:number, y:number) {
this.x = x;
this.y = y;
}
}
function createPoint(
PointClass: typeof Point, // 接收的是 Point 类的本身,不是 Point 类的实例对象,此时可以使用 typeof 获取类的类型
x:number,
y:number,
):Point {
return new PointClass(x, y);
}
let point1:Point = createPoint(Point, 1, 2);
let point2:Point = createPoint(Point, 3, 4);
本质上,类不过是构造函数的语法糖,因此也可以用构造函数的类型签名描述类的类型
构造函数的类型签名相比于普通函数,主要区别在于前面多了 new
关键字来进行修饰
// 普通函数的类型签名有两种写法,因此构造函数的类型签名也是有两种写法,一种是简写版,一种是完整版
type PointType1 = new (x:number, y:number) => Point; // 简写版
type PointType2 = { // 完整版
new (x:number, y:number): Point;
}
function createPoint(
PointClass: PointType1, // 或者是 PointType2
x:number,
y:number,
):Point {
return new PointClass(x, y);
}
(3)静态成员
之前介绍的类中定义的属性和方法,类本身是无法使用的,只能由实例化对象使用
但是有些场景下,一些属性和方法更适合由类本身使用嘞,例如:
- 类的所有实例需要共享某些属性或方法
- 某些属性或方法是与类的实例对象无关
这时候就需要定义静态成员,静态成员只由类本身使用,并不能由实例化对象使用
可以使用 static
关键字定义静态成员,静态成员有二,包括静态属性和静态方法
class Test {
// 实例属性
x:number = 0;
// 静态属性
static y:number = 0;
// 实例方法
printX() {
console.log(this.x);
}
// 静态方法
static printY() {
console.log(this.y);
}
}
Test.x; // 编译报错
Test.y; // 编译正常
Test.printX(); // 编译报错
Test.printY(); // 编译正常
let test = new Test();
test.x; // 编译正常
test.y; // 编译报错
test.printX(); // 编译正常
test.printY(); // 编译报错
3、继承 (extends
)
(1)继承
继承是面向对象编程中的重要概念,允许一个类(子类)基于另外一个类(父类)定义
子类能继承父类的属性和方法,并且能在此基础上添加自己的属性和方法
可以用 extends
关键字实现,在该关键字之后可指定当前类继承的父类
class Father {
greeting:string = 'Hi.';
sayHi() {
console.log(this.greeting);
}
}
class Son extends Father { // 可以用 extends 关键字实现继承
identity:string = 'I am son.';
identify() {
console.log(this.greeting, this.identity);
}
}
let f = new Father();
let s = new Son();
s.sayHi(); // 子类可以继承父类的属性和方法,输出为:Hi.
s.identify(); // 子类也能添加自己的属性和方法,输出为:Hi. I am son.
// 根据结构类型原则
// 任何可以使用父类的地方都可以使用子类代替 ,但反之不行
let a:Father = f;
a = s; // 编译正常
let b:Son = s;
b = f; // 编译报错
子类可定义与父类同名的属性和方法,此时定义的属性和方法会覆盖父类的属性和方法
但是要注意,这些同名的属性和方法要兼容原来的类型声明,否则编译器会报错不通过
class Father {
greeting:string = 'Hi.';
sayHi() {
console.log(this.greeting);
}
}
class Son extends Father {
greeting:string = 'Hello.'; // 同名属性
identity:string = 'I am son.';
sayHi() { // 同名方法
console.log(this.greeting, 'I am new.');
}
identify() {
console.log(this.greeting, this.identity);
}
}
let f = new Father();
let s = new Son();
f.sayHi(); // Hi.
s.sayHi(); // Hello. I am new.
s.identify(); // Hello. I am son.
子类方法中还能调用父类方法,此时只要使用 super
关键字即可
-
构造函数中,调用方式为
super()
,此时会调用 父类的构造函数而且要注意,子类如果有 构造函数 ,构造函数中 一定要有
super()
-
普通方法中,调用方式为
super.父类方法()
class Father {
greeting:string;
constructor(greeting) {
this.greeting = greeting;
}
sayHi() {
console.log(this.greeting);
}
}
class Son extends Father {
greeting:string;
identity:string;
constructor(greeting, identity) {
super(greeting); // 调用父类的构造函数,如果缺少,就会报错
this.identity = identity;
}
sayHi() {
super.sayHi(); // 调用父类的普通方法,需要注意,虽然此时执行的是父类方法的逻辑,但是其中的 this 指向子类
console.log(this.greeting, 'I am new.');
}
identify() {
super.sayHi(); // 调用父类的普通方法,需要注意,虽然此时执行的是父类方法的逻辑,但是其中的 this 指向子类
console.log(this.greeting, this.identity);
}
}
let f = new Father('Hi.');
let s = new Son('Hello.', 'I am son.');
f.sayHi(); // Hi.
s.sayHi(); // Hello. (注意这里是 Hello,而不是 Hi)
// Hello. I am new.
s.identify(); // Hello. (注意这里是 Hello,而不是 Hi)
// Hello. I am son.
(2)访问修饰符
为了控制类中成员对外部代码的可见性,TypeScript 提供有三种访问修饰符
这些修饰符对类本身以及类继承后成员的访问都有着一定的约束
-
public
表示公有成员,该成员既能在类内部访问,也能通过类实例访问public
成员可被继承,且继承后的可访问性不变 -
protected
表示保护成员,该成员可以在类内部访问,不能通过类实例访问protected
成员可被继承,且继承后的可访问性不变,但也可以将其设置为public
-
private
表示私有成员,该成员可以在类内部访问,不能通过类实例访问private
成员不可继承,且子类定义父类私有成员的同名成员会导致报错
需要注意的是 ,如果没有设置任何访问修饰符,默认就是 public
class Father {
public x = 'x'; // 公有成员
protected y = 'y'; // 保护成员
private z = 'z'; // 私有成员
// 类内部
testF() {
console.log(this.x); // 编译正常,公有成员可以在类内部访问
console.log(this.y); // 编译正常,保护成员可以在类内部访问
console.log(this.z); // 编译正常,私有成员可以在类内部访问
}
}
// 类实例
let f = new Father();
f.x; // 编译正常,公有成员可以用类实例访问
f.y; // 编译报错,保护成员不能用类实例访问
f.z; // 编译报错,私有成员不能用类实例访问
class Son extends Father {
// 继承后:
// 公有成员 x 可被继承,且其可访问性仍为 public
// 保护成员 y 可被继承,且其可访问性仍为 protected
// 私有成员 z 不可继承
// 等价于:
// public x = 'x'; // 公有成员
// protected y = 'y'; // 保护成员,此时可以将其改为公有成员:public y = 'y';
// 类内部
testS() {
console.log(this.x); // 编译正常,公有成员可以在类内部访问
console.log(this.y); // 编译正常,保护成员可以在类内部访问
console.log(this.z); // 编译错误,相当于无定义
}
}
// 类实例
let s = new Son();
s.x; // 编译正常,公有成员可以用类实例访问
s.y; // 编译报错,保护成员不能用类实例访问
s.z; // 编译报错,相当于无定义
4、实现 (implements
)
我们知道,type
和 interface
都能用于定义一个类型声明
那么其实,type
和 interface
也能用对象形式的类型声明为类指定一组检查条件
type typeC = {
p1: number;
p2: string;
f1: (x:number, y:number) => number;
}
interface interfaceC {
p1: number;
p2: string;
f1: (x:number, y:number) => number;
}
在定义类时,可以用 implements
指定检查条件,如有多个,则用逗号分隔
在实现类时,需要满足这些条件, 具体来说:
- 类需要实现检查条件中的所有成员,且成员的类型也要满足条件
- 类可以定义检查条件中没有的成员
class C implements typeC {
p1: number;
p2: string;
f1(x:number, y:number):number { return x + y; }
f2(x:string, y:string):string { return x + y; }
}
5、抽象 (abstract
)
在定义类时,如果在类的前面加上 abstract
关键字,则表示该类是抽象类
抽象类无法被实例化,故只能作为父类使用, 普通类可以继承该类并实例化
// 抽象类
abstract class Person {
name: string;
age : number;
}
let p = new Person(); // 编译错误,抽象类无法实例化
// 抽象类
// 抽象类可以继承抽象类
abstract class Doctor extends Person {
}
let d = new Doctor(); // 编译错误,抽象类无法实例化
// 普通类
// 普通类可以继承抽象类
class Lawyer extends Person {
}
let l = new Lawyer(); // 编译正常,普通类可以实例化
在抽象类中,如果在成员前面加上 abstract
关键字,则表示成员是抽象成员
抽象成员不能有具体实现,而普通子类则必须 实现所有的抽象成员,否则报错
abstract class Person {
// 抽象成员只能存在于抽象类
abstract name: string;
abstract age : number;
abstract addF(x:number, y:number):number; // 抽象成员不能有具体实现
}
// 抽象子类,可以不实现抽象成员
abstract class Doctor extends Person {
}
// 普通子类,必须要实现抽象成员
class Lawyer extends Person {
name: string;
age : number;
addF (x:number, y:number):number { return x + y; }
}
好啦,本文到此结束,感谢您的阅读!
如果你觉得这篇文章有需要修改完善的地方,欢迎在评论区留下你宝贵的意见或者建议
如果你觉得这篇文章还不错的话,欢迎点赞、收藏、关注,你的支持是对我最大的鼓励 (/ω\)