TypeScript---class类型
一.简介
TypeScript 完全支持 ES2015 中引入的 class
关键字。
与其他 JavaScript 语言功能一样,TypeScript 添加了类型注释和其他语法,以允许你表达类和其他类型之间的关系。
1.字段
(1).在申明时同时给出类型
class Person {name: string;age: number;
}
(2).申明时不给出类型
如果不给出类型,那么typescript会自动推断为any类型的数据
class Person {name; // any类型age; // any类型
}
(3).声明时给出初值
typescript会自动推断出来数据的类型是什么,比如下面的就会推断出来数据的类型为string与number
class Person {name = "寻谬~流光"; // stringage = 30; // number
}
(4).打开了TypeScript 的配置项strictPropertyInitialization
只要打开,就会检查属性是否设置了初值,如果没有就报错。
如果你打开了这个设置,但是某些情况下,不是在声明时赋值或在构造方法里面赋值,为了防止这个设置报错,可以使用非空断言。
class person {name!: string;age!: number;
}
属性名后面添加了感叹号,表示这两个属性肯定不会为空,所以 TypeScript 就不报错了
2.readonly 修饰符
属性名前面加上 readonly 修饰符,就表示该属性是只读的。实例对象不能修改这个属性。
- 属性前面有 readonly 修饰符,实例对象修改这个属性就会报错。
- 构造方法内部可以设置只读属性的初值
- 构造方法可以修改只读属性的值
class Person {readonly name = "寻谬~流光";readonly age!: number;// 哎构造函数内部可以自由修改readonly属性,不管是已经赋值了的,或者是没有赋值的数据类型constructor(age: number) {this.age = age;this.age = 20;}
}const p = new Person(18);
console.log(p.name); // 寻谬~流光
console.log(p.age); // 20p.age = 25; // 编译器报错, 不能对readonly属性重新赋值
3.构造器
constructor
方法是 class 的一个特殊方法,用于创建和初始化该类的对象实例。
类构造函数与函数非常相似。你可以添加带有类型注释、默认值和重载的参数
class Person {name = "寻谬~流光";age!: number;//通过给参数设置默认值,实现类似重载的效果。constructor(age: number = 18, name: string = "寻谬~流光") {this.age = age;this.name = name;}
}let p1 = new Person();
let p2 = new Person(20, "寻觅~流光");
let p3 = new Person(undefined, "你好");console.log(p1); // Person { name: '寻谬~流光', age: 18 }
console.log(p2); // Person { name: '寻觅~流光', age: 20 }
console.log(p3); // Person { name: '你好', age: 18 }
4.方法
类上的函数属性称为方法。
类的方法就是普通函数,类型声明方式与函数一致。
class Person {name = "寻谬~流光";age!: number;constructor(age: number = 18, name: string = "寻谬~流光") {this.age = age;this.name = name;}// 不添加函数返回值的类型,typescript会自动推导出来这个函数返回的数据类型((method) Person.add_age(num: number): number)add_age(num: number) {this.age += num;return this.age;}// 添加类型注释low_age(num: number = 1): number {this.age -= num;return this.age;}
}
5.获取器/设置器
存取器(accessor)是特殊的类方法,包括取值器(getter)和存值器(setter)两种方法。
它们用于读写某个属性,取值器用来读取属性,存值器用来写入属性。
class C {_name = "";get name() {return this._name;}set name(value) {this._name = value;}
}
上面示例中,get name()
是取值器,其中get
是关键词,name
是属性名。外部读取name
属性时,实例对象会自动调用这个方法,该方法的返回值就是name
属性的值。
set name()
是存值器,其中set
是关键词,name
是属性名。外部写入name
属性时,实例对象会自动调用这个方法,并将所赋的值作为函数参数传入。
(1).如果某个属性只有get
方法,没有set
方法,那么该属性自动成为只读属性。
class Person {_name: string;_age: number;constructor(name: string, age: number) {(this._name = name), (this._age = age);}// name为只读属性get name(): String {return this._name + "___ABC";}
}
const p = new Person("Tom", 20);
console.log(p.name); // Tom___ABCp.name = "Jerry"; // 报错,不能修改只读属性
(2).set
方法的参数类型,必须兼容get
方法的返回值类型,否则报错。
class Person {_name: string;_age: number;constructor(name: string, age: number) {(this._name = name), (this._age = age);}get name(): string {return this._name + "___ABC";}// 正确写法// set name(value: string) {// this._name = value;// }// 报错set name(value: number) {this._name = value;}
}
(3).get
方法与set
方法的可访问性必须一致,要么都为公开方法,要么都为私有方法。
正确写法:
class Person {_name: string;_age: number;constructor(name: string, age: number) {(this._name = name), (this._age = age);}private get name(): string {return this._name + "___ABC";}private set name(value: string) {this._name = value;}
}
错误写法:
class Person {_name: string;_age: number;constructor(name: string, age: number) {(this._name = name), (this._age = age);}private get name(): string {return this._name + "___ABC";}// Get 访问器必须至少具有与 Setter 相同的可访问性public set name(value: string) {this._name = value;}
}
6.索引签名
类可以声明索引签名;这些工作与 其他对象类型的索引签名 相同:
类允许定义属性索引。
class MyClass {[s: string]: boolean | ((s: string) => boolean);get(s: string) {return this[s] as boolean;}
}
上面示例中,[s:string]
表示所有属性名类型为字符串的属性,它们的属性值要么是布尔值,要么是返回布尔值的函数。
注意,由于类的方法是一种特殊属性(属性值为函数的属性),所以属性索引的类型定义也涵盖了方法。如果一个对象同时定义了属性索引和方法,那么前者必须包含后者的类型。
class MyClass {[s: string]: boolean;f() {// 报错return true;}
}
上面示例中,属性索引的类型里面不包括方法,导致后面的方法f()
定义直接报错。正确的写法是下面这样。
class MyClass {[s: string]: boolean | (() => boolean);f() {return true;}
}
属性存取器等同于方法,也必须包括在属性索引里面。
class MyClass {[s: string]: boolean;get(s: string) {// 报错return this[s] as boolean;}
}
上面示例中,属性索引没有给出方法的类型,导致get()
方法报错。
二.类的 interface 接口
1.implements 关键字
interface 接口或 type 别名,可以用对象的形式,为 class 指定一组检查条件。然后,类使用 implements 关键字,表示当前类满足这些外部类型条件的限制。
interface Person {name: string;age: number;
}// 或者这种写法
// type Person = {
// name: string;
// age: number;
// };class Student implements Person {name: string;age: number;constructor(name: string, age: number) {this.name = name;this.age = age;}
}
你可以使用 implements
子句来检查一个类是否满足特定的 interface
。如果一个类未能正确实现它,则会触发错误:
interface Pingable {ping(): void;
}class Sonar implements Pingable {ping() {console.log("ping!");}
}class Ball implements Pingable {pong() {console.log("pong!");}
}
implements
关键字后面,不仅可以是接口,也可以是另一个类。这时,后面的类将被当作接口。
class Person {name: string;age: number;add: (value: number) => number;
}// 实现接口
class Student implements Person {name: string;age: number;constructor(name: string, age: number) {this.name = name;this.age = age;}add(value: number) {this.age += value;return this.age;}
}
注意,interface 描述的是类的对外接口,也就是实例的公开属性和公开方法,不能定义私有的属性和方法。这是因为 TypeScript 设计者认为,私有属性是类的内部实现,接口作为模板,不应该涉及类的内部代码写法。
2.实现多个接口
类可以实现多个接口(其实是接受多重限制),每个接口之间使用逗号分隔。
interface student {//...
}
interface teacher {// ...
}// 继承多个接口
class class_people implements student, teacher {// ...
}
3.类与接口的合并
TypeScript 不允许两个同名的类,但是如果一个类和一个接口同名,那么接口会被合并进类。
interface person {name: string;
}
class person {age: number;adress: string;id: number;
}const p: person = {name: "Tom",age: 25,adress: "China",id: 123456,
};console.log(p.name); // Tom
就比如上
定义了person的接口与person的类,接口定义的会被合并到class的类型定义中去
三.类继承
1.extends
从句
类可能来自基类。派生类具有其基类的所有属性和方法,还可以定义额外的成员。
class father {name: string;age: number;
}
class son extends father {name: string;age: number;adress: string;
}
let s = new son();
s.name = "Tom";
s.age = 20;
s.adress = "China";
console.log(s);
super(name, age)
:在子类构造函数中,必须先调用父类的构造函数,才能使用this
。- 如果子类重写了父类方法,也可以在方法中用
super.方法名()
调用父类的方法。
2.super语句
super
语句用于在子类中调用父类的构造函数或父类的方法。
class Father {name: string;age: number;constructor(name: string, age: number) {this.name = name;this.age = age;}
}class Son extends Father {adress: string;constructor(name: string, age: number, adress: string) {super(name, age); // 调用父类构造函数this.adress = adress;}
}const s = new Son("Tom", 20, "China");
3.override语句
在 TypeScript 中,override
关键字用于明确表示子类正在重写父类的成员方法或属性。这样可以让编译器检查你是否真的在重写父类已有的方法,防止拼写错误或父类没有该方法时出错。
class Father {name: string;age: number;constructor(name: string, age: number) {this.name = name;this.age = age;}sayHello() {console.log(`Hello, I am ${this.name}`);}
}class Son extends Father {adress: string;constructor(name: string, age: number, adress: string) {super(name, age); // 调用父类构造函数this.adress = adress;}// 重写父类方法override sayHello() {super.sayHello(); // 可选:调用父类方法console.log(`I live in ${this.adress}`);}
}const s = new Son("Tom", 20, "China");
s.sayHello();
// 输出:
// Hello, I am Tom
// I live in China
override
只能用于重写父类已有的方法或属性。- 如果父类没有 sayHello 方法,写 override sayHello() 会报错,防止误写。
- 推荐在所有重写父类成员时加上
override
,代码更规范、安全。
四.成员可见性
类的内部成员的外部可访问性,由三个可访问性修饰符(access modifiers)控制:public
、private
和protected
。
这三个修饰符的位置,都写在属性或方法的最前面。
1.public
(1). 基本用法
public
修饰符表示这是公开成员,外部可以自由访问。
正常情况下,除非为了醒目和代码可读性,public
都是省略不写的。
class Person {public name: string; // 显式声明为publicage: number; // 隐式public(默认)public constructor(name: string, age: number) {this.name = name;this.age = age;}public greet() { // 方法默认也是publicreturn `Hello, I'm ${this.name}`;}
}const person = new Person("Alice", 30);
console.log(person.name); // 合法:外部可访问public成员
person.greet(); // 合法
2.protected
(1). 基本用法
protected
修饰符表示该成员是保护成员,只能在类的内部使用该成员,实例无法使用该成员,但是子类内部可以使用。
class Animal {protected name: string;constructor(name: string) {this.name = name;}protected move(distance: number) {console.log(`${this.name} moved ${distance}m.`);}
}class Dog extends Animal {constructor(name: string) {super(name);}bark() {console.log(`${this.name} barks!`); // 合法:子类可访问protected成员this.move(5); // 合法:子类可调用protected方法}
}const dog = new Dog("Buddy");
// console.log(dog.name); // 错误:实例无法访问protected成员
// dog.move(10); // 错误
dog.bark(); // 合法
(2).子类不仅可以拿到父类的保护成员,还可以定义同名成员。
class Vehicle {protected speed: number = 0;protected accelerate(value: number) {this.speed += value;}
}class Car extends Vehicle {protected speed: number = 10; // 子类可以定义同名protected成员override accelerate(value: number) {this.speed += value * 2; // 子类可以重写protected方法}
}
(3).在类的外部,实例对象不能读取保护成员,但是在类的内部可以。
3.private
(1). 基本用法
private
修饰符表示私有成员,只能用在当前类的内部,类的实例和子类都不能使用该成员。
class BankAccount {private balance: number = 0;constructor(initialDeposit: number) {this.balance = initialDeposit;}public deposit(amount: number) {this.balance += amount; // 合法:类内部可访问private成员}public getBalance() {return this.balance; // 合法}
}const account = new BankAccount(1000);
// console.log(account.balance); // 错误:无法访问private成员
account.deposit(500);
console.log(account.getBalance()); // 合法:通过public方法访问
(2).子类不能定义父类私有成员的同名成员。
class Base {private code: string = "123";
}class Derived extends Base {// private code: number = 456; // 错误:不能重定义父类私有成员
}
(3).如果在类的内部,当前类的实例可以获取私有成员。
4.实例属性的简写形式
在构造函数参数前加上
public
、protected
或private
,TypeScript 会自动为你声明并初始化同名实例属性。这种写法更简洁,常用于数据类或 DTO(数据传输对象)。
这样可以让代码更简洁、易读。
class Config {constructor(public apiKey: string,public timeout: number = 5000,private debug?: boolean) {}
}const config = new Config("123-456");
console.log(config.timeout); // 输出: 5000 (默认值)
五.静态成员
类的内部可以使用static
关键字,定义静态成员。
静态成员的特点:
- 包括静态属性和静态方法,只能通过类名直接访问,无法通过实例调用。
- 类加载时创建,仅一份
- 子类可以继承父类的静态成员,也可以重写静态方法
class Person {static value = 1;constructor(public name: string) {}log_value() {console.log(Person.value);}add_value(num: number) {Person.value += num;}
}// 只可以通过类名直接访问到静态属性和方法
// 这里静态方法同样如此,就不写了
console.log(Person.value);let p: Person = new Person("Tom");
let p2: Person = new Person("Jerry");p.log_value(); // 1
p2.log_value(); // 1p.add_value(2);p.log_value(); // 3
p2.log_value(); // 3p2.add_value(3);p.log_value(); // 6
p2.log_value(); // 6
六.抽象类,抽象成员
TypeScript 允许在类的定义前面,加上关键字abstract
,表示该类不能被实例化,只能当作其他类的模板。这种类就叫做“抽象类”(abastract class)。
1.抽象类只能当作基类使用,用来在它的基础上定义子类。
不能实例化抽象类
abstract class Animal {constructor(public name: string, public age: number) {}
}// const Animal_1 = new Animal("Animal", 1); // 错误! 不能实例化抽象类class Dog extends Animal {constructor(name: string, age: number, public breed: string) {super(name, age); // 实现构造函数}bark() {console.log(`${this.name} is barking.`);}
}
2.抽象类的子类也可以是抽象类,也就是说,抽象类可以继承其他抽象类。
abstract class Animal {constructor(public name: string, public age: number) {}
}abstract class Dog extends Animal {constructor(name: string, age: number, public breed: string) {super(name, age);}sound(): void {console.log("W-T-F !");}
}class Labrador extends Dog {constructor(name: string, age: number) {super(name, age, "Labrador");}bark(): void {console.log("Woof, woof!");}
}const myDog = new Labrador("Buddy", 3);
myDog.sound(); // 输出: W-T-F !
3.抽象成员
如果抽象类的属性前面加上abstract
,就表明子类必须给出该方法的实现。
abstract class Animal {constructor(public name: string, public age: number) {}abstract makeSound(): void; // 抽象成员函数
}class Dog extends Animal {// 实现抽象成员函数makeSound() {console.log("w t f!");}
}
(1)抽象成员只能存在于抽象类,不能存在于普通类。
(2)抽象成员不能有具体实现的代码。也就是说,已经实现好的成员前面不能加
abstract
关键字。(3)抽象成员前也不能有
private
修饰符,否则无法在子类中实现该成员。(4)一个子类最多只能继承一个抽象类。
总之,抽象类的作用是,确保各种相关的子类都拥有跟基类相同的接口,可以看作是模板。其中的抽象成员都是必须由子类实现的成员,非抽象成员则表示基类已经实现的、由所有子类共享的成员。
七.this 问题
在类中,this
始终指向当前实例对象,但这个指向会受到调用方式的影响。
- 当通过
实例.方法()
调用时,this
正常指向实例; - 当方法被单独提取(如赋值给变量、作为回调)时,
this
会丢失指向(变成undefined
或全局对象)。
这是因为类的方法默认定义在原型上,调用时需要依赖调用者来确定 this
的指向(即 “谁调用,this
指向谁”)。
1.常见的this场景问题
(1).方法作为回调函数时,this
丢失
原因:callback
只是一个函数引用,调用时没有 “调用者”,this
无法指向 user
实例。
class User {name: string = "Alice";sayHi() {console.log(`Hi, I'm ${this.name}`); // 这里的 this 会在回调中丢失!}
}const user = new User();
// 直接调用:this 指向 user 实例(正常)
user.sayHi(); // 输出:Hi, I'm Alice// 将方法提取为回调函数
const callback = user.sayHi;
// 单独调用时,this 指向 undefined(严格模式下)
callback(); // 报错:Cannot read property 'name' of undefined
(2).事件监听中的 this
丢失
原因:DOM 事件回调中,this
默认指向触发事件的 DOM 元素,覆盖了类实例的 this
。
class Button {label: string = "Click me";onClick() {console.log(`Clicked ${this.label}`); // 事件触发时,this 指向触发事件的 DOM 元素,而非实例}
}const btn = new Button();
// 给按钮绑定点击事件(假设页面有个 id 为 "btn" 的按钮)
document.getElementById("btn")?.addEventListener("click", btn.onClick);// 当点击按钮时:
// 输出:Clicked undefined(因为 this 指向按钮 DOM 元素,而非 Button 实例)
(3).箭头函数与普通方法的 this
差异
原因:箭头函数没有自己的 this
,它的 this
是定义时捕获的上下文(此处即类实例)。
class Test {value: number = 10;// 普通方法:this 依赖调用者normalMethod() {console.log(this.value);}// 箭头函数方法:this 固定指向实例arrowMethod = () => {console.log(this.value);};
}const test = new Test();
const normal = test.normalMethod;
const arrow = test.arrowMethod;normal(); // 报错:this 丢失
arrow(); // 正常输出:10(this 始终指向 test 实例)
2.解决 this
指向问题的 3 种方案
(1).在构造函数中绑定 this
(最常用)
通过 this.方法 = this.方法.bind(this)
强制绑定 this
到实例:
class User {name: string = "Alice";constructor() {// 绑定 this 到当前实例this.sayHi = this.sayHi.bind(this);}sayHi() {console.log(`Hi, I'm ${this.name}`);}
}const user = new User();
const callback = user.sayHi;
callback(); // 正常输出:Hi, I'm Alice(this 已绑定)
(2).使用箭头函数定义方法(简洁)
直接将方法定义为箭头函数,利用箭头函数的 this
特性固定指向:
class Button {label: string = "Click me";// 箭头函数方法:this 永远指向实例onClick = () => {console.log(`Clicked ${this.label}`);};
}const btn = new Button();
document.getElementById("btn")?.addEventListener("click", btn.onClick);
// 点击时输出:Clicked Click me(this 正确指向 Button 实例)
(3).调用时通过 call
/apply
指定 this
在调用方法时,用 call
或 apply
手动指定 this
:
class User {name: string = "Bob";sayHi() {console.log(`Hi, I'm ${this.name}`);}
}const user = new User();
const callback = user.sayHi;// 调用时用 call 绑定 this
callback.call(user); // 输出:Hi, I'm Bob
八.泛型类
类也可以写成泛型,使用类型参数。关于泛型的详细介绍
class person<T> {name: T;age: number;constructor(name: T, age: number) {this.name = name;this.age = age;}
}let p1: person<string> = new person("Tom", 25);
console.log(p1.name); // output: Tom
注意,静态成员不能使用泛型的类型参数。
下一篇:TypeScript---泛型