类
基本使用
class Person {
// 这么定义是会报错的,因为ts要求我们定义的属性必须有初始值
// 因为此时使用的是默认的构造器,创建实例的时候是new Person()
// 并没有给我们传递name和age的具体值,所以在某些时候使用的时候,name和age的值可能是undefined
name: string
age: number
eating() {
console.log('eating')
}
}
// 第一种初始化的方式
class Person {
name: string = 'Klaus'
age: number = 23
eating() {
console.log('eating')
}
}
// 定义构造器时 只能使用new Person() 即缺省构造器
const per = new Person()
// 可以在之后修改对应的值
per.name = 'Alex'
// 第二种初始化的方式
class Person {
name: string
age: number
// 定义构造器时 使用new Person() 会报错
constructor(name: string, age: number) {
this.name = name
this.age = age
}
eating() {
console.log('eating')
}
}
继承
// 继承的作用 属性和方法的复用
class Person {
name: string
age: number
constructor(name: string, age: number) {
this.name = name
this.age = age
}
eating() {
console.log('person eating')
}
}
// 使用extends关键字 来实现继承
class Student extends Person{
sno: number
constructor(name: string, age: number, sno: number) {
// 调用父类的构造函数,完成父类继承过来的属性和方法的初始化操作
// 在子类的构造函数中,必须在第一次使用this关键字之前执行super方法
super(name, age)
this.sno = sno
}
eating() {
// 使用super关键字 调用父类的方法
super.eating()
console.log('student eating')
}
study() {
console.log('study')
}
}
const stu = new Student('Klaus', 23, 1810166)
console.log(stu.name, stu.age, stu.sno)
stu.study()
stu.eating()
多态
class Animal {
action() {
console.log('animal action')
}
}
class Dog {
action() {
console.log('dog running')
}
}
class Bird {
action() {
console.log('bird flying')
}
}
// 多态 --- 通过'父类引用指向子类对象'的方式,使不同对象应用于同一种操作的时候,可以有不同的解释,产生不同的执行结果
// 所以继承是实现多态的前提条件
function checkAnimalAction(animal: Animal) {
animal.action()
}
checkAnimalAction(new Dog()) // dog running
checkAnimalAction(new Bird()) // bird flying
成员修饰符
在TypeScript中,类的属性和方法支持三种修饰符: public、private、protected
- public 修饰的是在任何地方可见、公有的属性或方法,默认编写的属性就是public
- private 修饰的是仅在同一类中可见、私有的属性或方法
- protected 修饰的是仅在类自身及子类中可见、受保护的属性或方法;
public
class Person {
public name: string
constructor(name: string) {
this.name = name
}
}
class Student extends Person {
// 没有写构造器,会存在一个默认的构造器
// 在默认的构造器中会将调用super方法初始化父类
// 并要求在new Student的时候传入name值以便于初始化赋值
getName() {
console.log(this.name)
}
}
const per = new Person('person')
const stu = new Student('Klaus')
// 外部可以访问public的属性或方法
console.log(per.name)
// 子类可以访问父类中public的属性或方法
stu.getName()
private
class Person {
private name: string
constructor(name: string) {
this.name = name
}
}
class Student extends Person {
getName() {
// 子类无法访问父类的private属性或方法
console.log(this.name)
}
}
const per = new Person('person')
// 外部无法访问类内部private的属性或方法
console.log(per.name)
protected
class Person {
protected name: string
constructor(name: string) {
this.name = name
}
}
class Student extends Person {
getName() {
// 子类可以访问父类的protected属性或方法
console.log(this.name)
}
}
const per = new Person('person')
const stu = new Student('Klaus')
// 外部无法访问类内部protected的属性或方法
console.log(per.name) // error
stu.getName()
readonly
使用readonly修饰符修饰的属性或方法 就是只读属性或方法
class Person {
// 此时name属性的类型是'Klaus'
readonly name = 'Klaus'
}
const per = new Person()
// per.name = 'Alex' ---> error
class Person {
// 虽然name属性设置为了只读 并且在这里赋值了
readonly name: string = 'Klaus'
// 但是在构造函数中,依旧是可以修改只读属性的值
// 也就是存在构造函数赋值的时候,属性的默认赋值会自动被忽略
// 所以如果属性在构造函数中赋值了, 就无需再声明类型的时候赋初始值
constructor(name: string) {
this.name = name
}
}
const per = new Person('Steven')
console.log(per.name) // Steven
class Person {
name: string
// readonly 修饰的变量如果是一个对象,那么他只能保证对象的引用地址是无法改变的
// 但是对象中的属性的值依旧是可以改变的
readonly firend?: Person
// 可以在构造函数中传递自身实例对象
constructor(name: string, firend?: Person) {
this.name = name
this.firend = firend
}
}
const per = new Person('Klaus', new Person('Steven'))
console.log(per.firend?.name) // => Steven
if (per.firend) {
per.firend.name = 'Alex'
}
console.log(per.firend?.name) // => Alex
访问器
class Person {
// 约定俗称 私有属性以下划线开头
private _name: string
constructor(name: string) {
this._name = name
}
}
const per = new Person('Klaus')
这个时候我们就无法获取到Person中的_name属性了,但是我们在实际时候的时候需要操作_name的值,但又不希望我们直接操作私有属性,可以使用如下两种方式解决:
class Person {
private _name: string
constructor(name: string) {
this._name = name
}
// 自定义获取和设置的方法,
// 调用的时候通过函数的方式去进行调用
getName() {
return this._name
}
setName(v: string) {
this._name = v
}
}
const per = new Person('Klaus')
per.setName('Alex')
console.log(per.getName()) // => Alex
class Person {
private _name: string
constructor(name: string) {
this._name = name
}
// 使用访问器 getter/setter (推荐)
// setter
set name(v) {
this._name = v
}
// getter
get name() {
return this._name
}
}
const per = new Person('Klaus')
// 获取的时候,通过访问器去进行访问(看似操作的是name属性,但是实际操作的是_name)
// 访问(获取)到per上的name属性后再进行赋值
per.name = 'Alex'
// 访问(获取)到per的name属性的值
console.log(per.name)
静态成员
使用static修饰符修饰的类的属性或类的方法 就叫做静态属性或静态方法,或者叫做类属性或类方法
class Person {
static country = 'China'
}
const per = new Person()
// 类属性和类方法是无法通过实例对象去进行访问的
// console.log(per.country) ---> error
// 类属性或类方法只能通过类来进行访问和调用
console.log(Person.country)
抽象成员
我们知道,继承是多态使用的前提。
所以在定义很多通用的调用接口(方法)时, 我们通常会让调用者传入父类,通过多态来实现更加灵活的调用方式
但是,父类本身可能并不需要对某些方法进行具体的实现,所以父类中定义的方法,,我们可以定义为抽象方法
抽象成员可以分为抽象属性和抽象方法。
抽象成员一般必须满足如下条件:
- 抽象方法,必须存在于抽象类中
- 抽象类是使用abstract声明的类
抽象成员一般有以下特点:
- 抽象类是不能被实例化
- 抽象类中的抽象方法必须在子类中被实现
function printArea(shape: Cricle | Rectange) {
console.log(shape.calcArea())
}
class Rectange {
private width: number
private height: number
constructor(width: number, height: number) {
this.width = width
this.height = height
}
calcArea() {
return this.width * this.height
}
}
class Cricle {
private radius: number
constructor(radius: number) {
this.radius = radius
}
calcArea() {
return Math.pow(this.radius, 2) * Math.PI
}
}
const rect = new Rectange(20, 30)
const cricle = new Cricle(10)
printArea(rect)
printArea(cricle)
此时printArea并不是一个完全通用的方法,因为本质上只要实现了calcArea方法的任何对象都可以调用printArea方法
所以可以对上述代码进行改进
// shape的类型为Shape 也就是传入的参数必须是Shape的实例Shape子类的实例对象
// 因为子类继承自Shape,而父类中实现了calcArea
// 所以可以认为printArea的参数都是实现了calcArea方法的对象
// ps: ts中,接口也可以实现多态,这边仅仅只是讨论通过继承实现多态的情况
function printArea(shape: Shape) {
console.log(shape.calcArea())
}
class Shape {
calcArea() {}
}
class Rectangel extends Shape { ... }
class Cricle extends Shape { ... }
但是这么编写代码,依旧是有瑕疵的
// 因为此时,下面的代码依旧是合法的
printArea(undefined) // undefined 是任何类型的子类型
printArea(new Shape()) // Shape可以被实例化,但是实际上Shape中的calcArea只是一个占位实现而已,没有实际意义
所以此时可以使用抽象方法来对该例子进行改进
function printArea(shape: Shape) {
console.log(shape.calcArea())
}
abstract class Shape {
abstract calcArea(): number
}
class Rectange extends Shape {
private width: number
private height: number
constructor(width: number, height: number) {
super()
this.width = width
this.height = height
}
calcArea() {
return this.width * this.height
}
}
class Cricle extends Shape {
private radius: number
constructor(radius: number) {
super()
this.radius = radius
}
calcArea() {
return Math.pow(this.radius, 2) * Math.PI
}
}
const rect = new Rectange(20, 30)
const cricle = new Cricle(10)
printArea(rect)
printArea(cricle)
// 抽象类中的抽象方法必须在子类中被实现,所以此时下面的代码就变得不合法了
printArea(new Shape())
类类型
类本身也是可以作为一个类型注解来进行使用的
class Person {
name: string
constructor(name: string) {
this.name = name
}
study() {
console.log('study in Person')
}
}
// Person 可以作为一个类型来进行使用
// 只要一个对象实现了Person中应该要实现的方法和属性后,这个对象就可以被赋值给一个Person类型的变量
// 所以可以认为实例对象的类型就是他们的类
const p: Person = {
name: 'Klaus',
study() {
console.log('studying')
}
}
所以在有的地方,我们可能会看到如下的代码编写方式
class Person {
name: string
constructor(name:string) {
this.name = name
}
study() {
console.log('study in Person')
}
}
function printUserName(per: Person) {
console.log(per.name)
}
printUserName(new Person('Alex'))
printUserName({
name: 'Klaus',
study() {
console.log('studying')
}
})
接口
接口是ts提供的另外一种定义对象类型的方式
// 之前定义对象类型的时候,可以使用关键字type来为类型起别名
type userInfoType = {
readonly name: string,
age?: number
}
const user1: userInfoType = {
name: 'Klaus',
age: 23
}
const user2: userInfoType = {
name: 'Alex'
}
// 我们定义对象类型的类型注解还可以使用接口
// 接口使用大写I开头 --- 这是一种通俗的规范,不是强制的
interface Iuser {
readonly name: string,
age?: number
}
// 接口的基本定义和使用和type关键字是基本一致的
// 接口也支出可选参数和只读属性
const user1: Iuser = {
name: 'Klaus',
age: 23
}
const user2: Iuser = {
name: 'Alex'
}
索引签名
在多数情况下,如果一个对象中的key值的类型都是一致的时候,例如数组,那么使用接口定义这个对象的时候,可以使用索引签名
// 此时这个接口作用和字符串数组的作用其实是差不多的
interface IArr {
// [] 中存放的是变量
// index是个形参,可以任意取名,但是最好见名知意
[index: number]: string
}
const names: IArr = ['Klaus']
const arr: IArr = {
0: 'Alex',
1: 'Steven'
}
// 接口不是只能用来定义数组,只要一个对象的属性名的类型是一致的时候,就可以使用索引签名
interface IUser {
[name: string]: number
}
const user: IUser = {
'Klaus': 23,
'Alex': 17
}
函数接口
函数接口是typescript中一种特殊的接口,函数接口是一种可执行接口类型
// 可以使用接口来定义函数
interface IAdd {
(num1: number, num2: number): number
}
const add: IAdd = (num1: number, num2: number) => num1 + num2
add(3, 4)
// 从语义化角度讲,定义对象类型的时候,使用类型别名的语义化比接口强
// 所以在定义函数类型的时候推荐使用类型别名,而定义普通对象类型的时候,推荐使用接口
interface IAdd {
(num1: number, num2: number): number
}
type addType = (num1: number, num2: number) => number
接口组合
接口组合方式1 --- 接口继承
interface ISwim {
swimming: () => void
}
interface IRun {
running: () => void
}
// 接口是可以多继承的
interface IAction extends ISwim, IRun {}
const action: IAction = {
swimming() {},
running() {}
}
class Student {
studying() {
console.log('studying')
}
}
// 特别的是,ts中接口是可以继承自类
// 这么做等价于 class Student implements IEat
interface IEat extends Student {
name: string
eatting: () => void
}
const stu: IEat = {
name: 'Klaus',
eatting: () => console.log('student eatting'),
studying: () => console.log('student studying')
}
接口组合方式2 --- 交叉类型
交叉类型和联合类型一样,都是ts中的组合类型
interface ISwim {
swimming: () => void
}
interface IRun {
running: () => void
}
// 定义一个新的类型,这个类型是IRun和ISwim这2个类型的交叉类型
// 也就是这2个接口中的所有属性和方法都必须要实现
type Action = IRun & ISwim
const action: Action = {
swimming() {},
running() {}
}
接口的实现
如果2个类中有公共的方法,没有公共的属性的时候,可以通过接口把公共的方法抽取出来单独定义,(如果还有属性或公共方法较多的时候推荐使用类的继承)
// 某一个对象实现了ISwim接口,就是某一个对象有swimming方法
interface ISwim {
swimming: () => void
}
interface IEat {
eatting: () => void
}
// 类只能单继承,但是可以实现多个接口
class Person implements ISwim, IEat {
swimming() {
console.log('person swimming')
}
eatting() {
console.log('person eatting')
}
}
interface ISwim {
swimming: () => void
}
class Person implements ISwim {
swimming() {
console.log('person swimming')
}
}
class Fish implements ISwim{
swimming() {
console.log('fish swimming')
}
}
// 实现接口的好处是可以将公共的方法抽取出来进行单独约束
// 例如 此处指代的就是 checkSwimAction的参数是一个实现了swimming方法的对象
function checkSwimAction(animal: ISwim) {
// 传入的animal是肯定拥有swimming方法的
animal.swimming()
}
checkSwimAction(new Person())
checkSwimAction(new Fish())
checkSwimAction({ swimming() {} })
面向接口编程
interface ISwim {
swimming: () => void
}
interface IRun {
running: () => void
}
class Animal {
}
// 一个类只能继承自一个类
// 但是一个类可以实现多个接口
class Person extends Animal implements ISwim, IRun {
// 所谓实现这个接口,就是需要在这个类中对这个接口进行实现
swimming() {}
running() {}
}
interface ISwim {
swimming: () => void
}
// 传入的参数只要是实现了ISwim这个接口的任意数据类型都可以
// 这种通过接口来限制传入的参数的方式就是面向接口编程
function checkAction(action: ISwim) {
action.swimming()
}
class Person implements ISwim {
swimming() {}
}
checkAction(new Person())
checkAction({swimming() {}})
Interface 和 Type的区别
interface和type都可以用来定义对象类型,但是他们是有区别的
interface IFoo {
name: string
}
interface IFoo {
age: number
}
// 可以定义同名接口,接口中的属性和方法会整合在一起
const foo: IFoo = {
name: 'Klaus',
age: 23
}
type IFoo = {
name: string
}
// error => 不能重复定义相同的type
type IFoo = {
age: number
}