Class 类
类的定义与继承
// 类里写属性与方法
class Person {
name = 'toimc'
getName() {
return this.name
}
}
const person = new Person()
console.log(person.getName()) // toimc
// 继承类,继承类属于字类,被继承的属于父类
class Teacher extends Person {
getTeacherName() {
return 'toimc Teacher'
}
// 子类可以重写父类的属性与方法
getName() {
// super 关键字指向了父类,可以直接调用父类。不会受到类重写的影响
return super.getName() + 'TTT'
}
}
const teacher = new Teacher()
console.log(teacher.getName()) // toimc
console.log(teacher.getTeacherName()) // toimc Teacher
typescript
修饰符
访回类型:
- private:允许在类内使用
- protected:允许在类内及继承的子类中使用
- public:允许在类的内外调用(默认)
自带方法:
- readonly:只读属性
- static:将方法挂载到类上而不是实例上,实例是没法访问和继承到的
public
public
表示公共的,用来指定在创建实例后可以通过实例访问的,也就是类定义的外部可以访问的属性和方法。
类中的属性默认是 public
class Point {
public x: number;
public y: number;
constructor(x: number, y: number) {
this.x = x;
this.y = y;
}
public getPosition() {
return `(${this.x}, ${this.y})`;
}
}
typescript
private
private
修饰符表示私有的,它修饰的属性在类的定义外面是没法访问的(注意:编译后的 JavaScript 仍然是普通属性,仅在类型系统内受保护):
class Parent {
private age: number;
constructor(age: number) {
this.age = age;
}
}
const p = new Parent(18);
console.log(p); // { age: 18 }
console.log(p.age); // error 属性“age”为私有属性,只能在类“Parent”中访问
console.log(Parent.age); // error 类型“typeof ParentA”上不存在属性“age”
class Child extends Parent {
constructor(age: number) {
super(age);
console.log(super.age); // 通过 "super" 关键字只能访问基类的公共方法和受保护方法
}
}
typescript
这里在 constructor
中访问 super
,这的 super
相当于父类本身,这里我们看到使用 private
修饰的属性,在子类中是没法访问的。
如果希望在运行时也具备真正的私有性,可以使用 ECMAScript 原生的 #
前缀语法(TypeScript 3.8+ 支持):
class Counter {
#value = 0;
increment() {
this.#value += 1;
return this.#value;
}
}
new Counter().#value; // error: 私有字段只能在类内部访问
typescript
protected
protected
修饰符是受保护修饰符,protected
修饰的成员在继承该类的子类中可以访问。
我们再来看下上面那个例子,把父类 Parent 的 age 属性的修饰符 private 替换为 protected:
class Parent {
protected age: number;
constructor(age: number) {
this.age = age;
}
protected getAge() {
return this.age;
}
}
const p = new Parent(18);
console.log(p.age); // error 属性“age”为私有属性,只能在类“ParentA”中访问
console.log(Parent.age); // error 类型“typeof ParentA”上不存在属性“age”
class Child extends Parent {
constructor(age: number) {
super(age);
console.log(super.age); // undefined
console.log(super.getAge());
}
}
new Child(18)
typescript
protected
还能用来修饰 constructor
构造函数,加了protected
修饰符之后,这个类就不能再用来创建实例,只能被子类继承。
需要用new.target
来自行判断,而 TS 则只需用 protected 修饰符即可:
class Parent {
protected constructor() {
//
}
}
const p = new Parent(); // error 类“Parent”的构造函数是受保护的,仅可在类声明中访问
class Child extends Parent {
constructor() {
super();
}
}
const c = new Child();
typescript
readonly
在类里可以使用readonly
关键字将属性设置为只读。
class UserInfo {
readonly name: string;
constructor(name: string) {
this.name = name;
}
}
const user = new UserInfo("toimc");
user.name = "haha"; // error Cannot assign to 'name' because it is a read-only property
typescript
设置为只读的属性,实例只能读取这个属性值,但不能修改。
constructor
constructor 构建函数,会在 new 实例的时候自动执行
// 以下两段代码相同, constructor 里,参数前加上public代表在之前已经声明过这个变量了
// 传统写法
class Person {
public name: string
constructor(name: string) {
this.name = name
}
}
const person = new Person('toimc')
// 简化写法
class Person {
// public name: string
constructor(public name: string) {
// this.name = name
}
}
const person = new Person('toimc')
console.log(person.name)
typescript
字类集成父类并使用 constructor 的话,必须先调用父类的 constructor ,并按照父类的参数规则进行
// super()代表调用父类的 constructor
// 如果父类没有使用constructor 字类需要调用一个空的super()
class Person {
constructor(public name: string) {}
}
class Teacher extends Person {
constructor(public age: number) {
super('toimc')
}
}
const teacher = new Teacher(28)
typescript
参数属性
之前的例子中,我们都是在类的定义的顶部初始化实例属性,在 constructor
里接收参数然后对实力属性进行赋值,我们可以使用参数属性来简化这个过程。参数属性简单来说就是在 constructor
构造函数的参数前面加上访问限定符,也就是前面讲的 public
、private
、protected
和 readonly
中的任意一个,我们来看例子:
class A {
constructor(name: string) {}
}
const a = new A("aaa");
console.log(a.name); // error 类型“A”上不存在属性“name”
class B {
constructor(public name: string) {}
}
const b = new B("bbb");
console.log(b.name); // "bbb"
typescript
可以看到,在定义类 B 时,构造函数有一个参数 name,这个 name 使用访问修饰符 public 修饰,此时即为 name 声明了参数属性,也就无需再显示地在类中初始化这个属性了。
静态属性
和 ES6 的类一样,在 TS 中一样使用static
关键字来指定属性或方法是静态的,实例将不会添加这个静态属性,也不会继承这个静态方法,可以使用修饰符和 static 关键字来指定一个属性或方法:
class Parent {
public static age: number = 18;
public static getAge() {
return Parent.age;
}
constructor() {
//
}
}
const p = new Parent();
console.log(p.age); // error Property 'age' is a static member of type 'Parent'
console.log(Parent.age); // 18
typescript
如果使用了 private 修饰道理和之前的一样:
class Parent {
public static getAge() {
return Parent.age;
}
private static age: number = 18;
constructor() {
//
}
}
const p = new Parent();
console.log(p.age); // error Property 'age' is a static member of type 'Parent'
console.log(Parent.age); // error 属性“age”为私有属性,只能在类“Parent”中访问。
typescript
可选类属性
TS 在 2.0 版本,支持可选类属性,也是使用?
符号来标记,来看例子:
class Info {
name: string;
age?: number;
constructor(name: string, age?: number, public sex?: string) {
this.name = name;
this.age = age;
}
}
const info1 = new Info("toimc");
const info2 = new Info("toimc", 18);
const info3 = new Info("toimc", 18, "man");
typescript
存储器
ES6 标准中的存值函数和取值函数,也就是在设置属性值的时候调用的函数,和在访问属性值的时候调用的函数,用法和写法和 ES6 的没有区别
class UserInfo {
private _fullName: string;
constructor() {}
get fullName() {
return this._fullName;
}
set fullName(value) {
console.log(`setter: ${value}`);
this._fullName = value;
}
}
const user = new UserInfo();
user.fullName = "toimc Li"; // "setter: toimc Li"
console.log(user.fullName); // "toimc Li"
typescript
做个小案例
通过 TS 创建一个 Demo 类,这个类只能被调用一次 思路:
- 不能在外部以 new Demo 的形式创建一个实例(将 constructor 设置为私有属性)
- 使用 static (将方法挂载到类上而不是实例上)来实现
- 使用 instance 方法来保存传入的值,并判断
class Demo {
private constructor(public name: string) {}
private static instance: Demo
static getInstance(name: string) {
if (!this.instance) {
this.instance = new Demo(name)
}
return this.instance
}
}
const demo1 = Demo.getInstance('toimc')
const demo2 = Demo.getInstance('toimca')
console.log(demo1.name)
console.log(demo2.name)
typescript
抽象类
特点:
- 只能被继承,不能实例化
- 抽象类里的抽象方法,不能够写具体实现
用法:抽象类和类内部定义抽象方法,使用abstract
关键字,我们先来看个例子:
abstract class People {
constructor(public name: string) {}
abstract printName(): void;
}
class Man extends People {
constructor(name: string) {
super(name);
this.name = name;
}
printName() {
console.log(this.name);
}
}
const m = new Man(); // error 应有 1 个参数,但获得 0 个
const man = new Man("toimc");
man.printName(); // 'toimc'
// 抽象类一般用来被其他类继承,而不直接用它创建实例。
const p = new People("toimc"); // error 无法创建抽象类的实例
typescript
我们再来看个例子:
abstract class People {
constructor(public name: string) {}
abstract printName(): void;
}
class Man extends People {
// error 非抽象类“Man”不会实现继承自“People”类的抽象成员"printName"
constructor(name: string) {
super(name);
this.name = name;
}
}
const m = new Man("toimc");
m.printName(); // error m.printName is not a function
typescript
通过上面的例子我们可以看到,在抽象类里定义的抽象方法,在子类中是不会继承的,所以在子类中必须实现该方法的定义。
2.0 版本开始,abstract
关键字不仅可以标记类和类里面的方法,还可以标记类中定义的属性和存取器:
abstract class People {
abstract _name: string;
abstract get insideName(): string;
abstract set insideName(value: string);
}
class Pp extends People {
_name: string;
insideName: string;
}
typescript
但是要记住,抽象方法和抽象存取器都不能包含实际的代码块。
实例类型
当我们定义一个类,并创建实例后,这个实例的类型就是创建他的类:
class People {
constructor(public name: string) {}
}
let p: People = new People("toimc");
typescript
当然了,创建实例的时候这指定 p 的类型为 People 并不是必须的,TS 会推断出他的类型。
虽然指定了类型,但是当我们再定义一个和 People 类同样实现的类 Animal,并且创建实例赋值给 p 的时候,是没有问题的:
class Animal {
constructor(public name: string) {}
}
let p = new Animal("lark");
typescript
所以,如果你想实现对创建实例的类的判断,还是需要用到instanceof
关键字。
扩展用法
类类型接口
使用接口可以强制一个类的定义必须包含某些内容,先来看个例子:
interface FoodInterface {
type: string;
}
class FoodClass implements FoodInterface {
// error Property 'type' is missing in type 'FoodClass' but required in type 'FoodInterface'
static type: string;
constructor() {}
}
typescript
上面接口 FoodInterface 要求使用该接口的值必须有一个 type 属性,定义的类 FoodClass 要使用接口,需要使用关键字implements
。
implements关键字用来指定一个类要继承的接口,如果是接口和接口、类和类直接的继承,使用extends;如果是类继承接口,则用implements。
tips:接口检测的是使用该接口定义的类创建的实例
所以上面例子中虽然定义了静态属性 type,但静态属性不会添加到实例上,所以还是报错,所以我们可以这样改:
interface FoodInterface {
type: string;
}
class FoodClass implements FoodInterface {
constructor(public type: string) {}
}
typescript
当然这个需求也可以使用抽象类实现:
abstract class FoodAbstractClass {
abstract type: string;
}
class Food extends FoodAbstractClass {
constructor(public type: string) {
super();
}
}
typescript
接口继承类
接口可以继承一个类,当接口继承了该类后,会继承类的成员,但是不包括其实现,也就是只继承成员以及成员类型。
接口还会继承类的private
和protected
修饰的成员,当接口继承的这个类中包含这两个修饰符修饰的成员时,这个接口只可被这个类或他的子类实现。
class A {
protected name: string;
}
interface I extends A {}
class B implements I {} // error Property 'name' is missing in type 'B' but required in type 'I'
class C implements I {
// error 属性“name”受保护,但类型“C”并不是从“A”派生的类
name: string;
}
class D extends A implements I {
getName() {
return this.name;
}
}
typescript
泛型中使用类类型
这里我们先来看个例子:
const create = <T>(c: { new (): T }): T => {
return new c();
};
class Info {
age: number;
}
create(Info).age;
create(Info).name; // error 类型“Info”上不存在属性“name”
typescript
在这个例子里,我们创建了一个一个 create 函数,传入的参数{ new (): T }
是一个构造函数的类型,返回的是创建的实例return new c()
,类型T
。
拆解来看:
- 参数
c
的类型定义中,new()
代表调用类的构造函数,它的类型T
也就是类创建实例后的实例的类型T
; return new c()
这里使用传进来的类c
创建一个实例并返回,返回的实例类型也就是函数的返回值类型。
所以通过这个定义,TS 就知道,调用 create
函数,传入的和返回的值都应该是同一个类类型。
PS: 如果上面的构造函数含有参数的话,比如包含一个 number
类型的参数时,我们可以这样定义 create 方法:
const create = <T>(c: { new (a: number): T; }, num: number): T => {
return new c(num);
}
class Info {
age: number;
constructor(num:number){
this.age = num
}
}
console.log(create(Info, 2222).age) // 2222
console.log(create(Info, 3333).num) // error Property 'num' does not exist on type 'Info'
typescript
↑