TypeScript面向对象
TypeScript面向对象
1、类
(1) 类的定义
我们来定义一个Person类:
使用class关键字来定义一个类;
我们可以声明一些类的属性:在类的内部声明类的属性以及对应的类型
如果类型没有声明,那么它们默认是any的;
我们也可以给属性设置初始化值;
在默认的strictPropertyInitialization模式下面我们的属性是必须初始化的,如果没有初始化,那么编译时就会报错;
如果我们在strictPropertyInitialization模式下确实不希望给属性初始化,可以使用 name!: string语法;
类可以有自己的构造函数constructor,当我们通过new关键字创建一个实例时,构造函数会被调用;
造函数不需要返回任何值,默认返回当前创建出来的实例;
类中可以有自己的函数,定义的函数称之为方法;
class Person {
name!: string;
age: number;
constructor(name: string, age: number) {
// this.name = name;
this.age = age;
}
running(){
console.log(this.name + " running");
}
eating() {
console.log(this.name + " eating");
}
}
(2) 类的继承
面向对象的其中一大特性就是继承,继承不仅仅可以减少我们的代码量,也是多态的使用前提。
我们使用extends关键字来实现继承,子类中使用super来访问父类。
我们来看一下Student类继承自Person:
Student类可以有自己的属性和方法,并且会继承Person的属性和方法;
在构造函数中,我们可以通过super来调用父类的构造方法,对父类中的属性进行初始化;
class Student extends Person {
sno: number;
constructor(name: string, age: number, sno: number) {
super(name, age);
this.sno = sno;
}
studying() {
console.log(this.name + " studying")
}
}
(3) 类的成员修饰符
在TypeScript中,类的属性和方法支持三种修饰符: public、private、protected
public修饰的是在任何地方可见、公有的属性或方法,默认编写的属性就是public的;private修饰的是仅在同一类中可见、私有的属性或方法;protected修饰的是仅在类自身及子类中可见、受保护的属性或方法;
// private
class Animal {
private name: string;
constructor(name: string) {
this.name = name;
}
}
const xm = new Animal('熊猫');
// Property.'name'is private and only accessible within
// console.log(xm.name)
// protected
class Person {
protected name: string;
constructor(name: string) {
this.name = name;
}
}
class Student extends Person {
constructor(name: string) {
super(name);
}
running() {
console.log(this.name + "running");
}
}
(4) 只读属性 readonly
- 如果有一个属性我们不希望外界可以任意的修改,只希望确定值后直接使用,那么可以使用readonly:
class Person {
private name: string;
constructor(name: string) {
this.name = name;
}
}
const ck = new Person('CoderKing');
// Cannot assign to.'name'because it is a read-only property.
// ck.name ="King"
(5) 存取器 getters/setters
- 在前面一些私有属性我们是不能直接访问的,或者某些属性我们想要监听它的获取(getter)和设置(setter)的过程,这个时候我们可以使用存取器。
class Person {
private _name: string;
get name(): string {
return this._name;
}
set name(value: string) {
this._name = value;
}
constructor(name: string) {
this.name = name;
}
}
const ck = new Person('CoderKing');
ck.name = 'king';
console.log(ck.name);
(6) 静态成员 static
前面我们在类中定义的成员和方法都属于对象级别的, 在开发中, 我们有时候也需要定义类级别的成员和方法。
在TypeScript中通过关键字static来定义:
class Student {
static time: string = "20:00";
static attendClass() {
console.log("去学习~");
}
}
console.log(Student.time);
Student.attendClass();
(7) 抽象类 abstract
我们知道,继承是多态使用的前提。
所以在定义很多通用的调用接口时, 我们通常会让调用者传入父类,通过多态来实现更加灵活的调用方式。
但是,父类本身可能并不需要对某些方法进行具体的实现,所以父类中定义的方法,,我们可以定义为抽象方法。
什么是 抽象方法? 在TypeScript中没有具体实现的方法(没有方法体),就是抽象方法。
抽象方法,必须存在于抽象类中;
抽象类是使用abstract声明的类;
抽象类有如下的特点:
- 抽象类是不能被实例的话(也就是不能通过new创建)
- 抽象方法必须被子类实现,否则该类必须是一个抽象类;
// 抽象类演练
function makeArea(shape: Shape) {
return shape.getArea();
}
abstract class Shape {
abstract getArea(): number;
}
class Rectangle extends Shape {
private width: number;
private height: number;
constructor(width: number, height: number) {
super();
this.width = width;
this.height = height;
}
getArea() {
return this.width * this.height;
}
}
class Circle extends Shape {
private r: number;
constructor(r: number) {
super();
this.r = r;
}
getArea() {
return this.r * this.r * 3.14;
}
}
const rectangle = new Rectangle(20, 30);
const circle = new Circle(10);
console.log(makeArea(rectangle));
console.log(makeArea(circle));
// makeArea(new Shape())
// makeArea(123)
// makeArea("123")
(8) 类的类型
- 类本身也是可以作为一种数据类型的:
class Person {
name: string;
constructor(name: string) {
this.name = name;
}
running() {
console.log(this.name + " running");
}
}
const p1 = new Person("coderking");
const p2: Person = {
name: "king",
running: function() {
console.log(`${this.name}正在跑步!`);
},
};
2、接口
(1) 接口声明
- 在前面我们通过type可以用来声明一个对象类型
type Point = {
x: number
y: number
}
- 对象的另外一种声明方式就是通过接口来声明
interface Point = {
x: number
y: number
}
(2) 可选属性
- 在接口中也能使用可选属性
interface Person {
name: string
age?: number
friend: {
name?: string
}
}
const p: Person = {
name: "CoderKing"
age: 18
friend: {
name: "king"
}
}
console.log(p.name)
console.log(p.frinend?.name)
(3) 只读属性
- 接口中也可以定义只读属性:
- 这样就意味着我们再初始化之后,这个值是不可以被修改的;
interface Person {
readonly name: string
age?: number
readonly friend?: {
name: string
}
}
const p: Person = {
name: "CoderKing"
age: 18
friend: {
name: "king"
}
}
// p.name = "king" /*不可以设置*/
// p.friend = {} /*不可以设置*/
// 可以更改不是只读属性的值
if(p.friend){
p.friend.name = "ck"
}
(4) 索引类型
- 前面我们使用interface来定义对象类型,这个时候其中的属性名、类型、方法都是确定的,但是有时候我们会遇到类似下面的对象:
// 通过interface来定义索引类型
interface IndexLanguage {
[index: number]: string
}
const frontLanguage: IndexLanguage = {
0: "HTML",
1: "CSS",
2: "JavaScript",
3: "Vue"
}
interface ILanguageYear {
[name: string]: number
}
const languageYear: ILanguageYear = {
"C": 1972,
"Java": 1995,
"JavaScript": 1996,
"TypeScript": 2014
}
(5) 函数类型
- 前面我们都是通过interface来定义对象中普通的属性和方法的,实际上它也可以用来定义函数类型:
interface CalcFn {
(n1: number, n2: number): number
}
function calc(num1: number, num2: number, calcFn: CalcFn) {
return calcFn(num1, num2)
}
const add: CalcFn = (num1, num2) => {
return num1 + num2
}
calc(20, 30, add)
- 当然,除非特别的情况,还是推荐大家使用类型别名来定义函数:
type CalcFn = (n1: number, n2: number) => number
(6) 接口继承
- 接口和类一样是可以进行继承的,也是使用extends关键字:
- 并且我们会发现,接口是支持多继承的(类不支持多继承)
interface ISwim {
swimming: () => void;
}
interface IFly {
flying: () => void;
}
interface IAction extends ISwim, IFly {
name: string;
}
const action: IAction = {
name: "action",
swimming() {},
flying() {},
};
(7) 接口实现
接口定义后,也是可以被类实现的:
如果被一个类实现,那么在之后需要传入接口的地方,都可以将这个类传入;
这就是面向接口开发;
interface ISwim {
swimming: () => void;
}
interface IEat {
eating: () => void;
}
// 类实现接口
class Animal {}
// 继承: 只能实现单继承
// 实现: 实现接口, 类可以实现多个接口
class Fish extends Animal implements ISwim, IEat {
swimming() {
console.log("Fish Swmming");
}
eating() {
console.log("Fish Eating");
}
}
class Person implements ISwim {
swimming() {
console.log("Person Swimming");
}
}
// 编写一些公共的API: 面向接口编程
function swimAction(swimable: ISwim) {
swimable.swimming();
}
// 1.所有实现了接口的类对应的对象, 都是可以传入
swimAction(new Fish());
swimAction(new Person());
swimAction({ swimming: function () {} });
(8) 交叉类型
有一种类型合并,叫交叉类型(Intersection Types):
交叉类似表示需要满足多个类型的条件;
交叉类型使用 & 符号;
我们来看下面的交叉类型:
表达的含义是number和string要同时满足;
但是有同时满足是一个number又是一个string的值吗?其实是没有的,所以MyType其实是一个never类型;
type MyType = number & string
- 在开发中,我们进行交叉时,通常是对对象类型进行交叉的:
interface ISwim {
swimming: () => void
}
interface IFly {
flying: () => void
}
type MyType1 = ISwim | IFly
type MyType2 = ISwim & IFly
const obj1: MyType1 = {
flying() {
}
}
const obj2: MyType2 = {
swimming() {
},
flying() {
}
}
(9) interface和type区别
我们会发现interface和type都可以用来定义对象类型,那么在开发中定义对象类型时,到底选择哪一个呢?
- 如果是定义非对象类型,通常推荐使用type,比如Direction、Alignment、一些Function;
如果是定义对象类型,那么他们是有区别的:
interface 可以重复的对某个接口来定义属性和方法;
而type定义的是别名,别名是不能重复的;
interface IFoo {
name: string
}
interface IFoo {
age: number
}
// type定义别名不能重复
type IBar = {
name: string
age: number
}
type IBar = {
}
3、泛型
(1) 认识泛型
软件工程的主要目的是构建不仅仅明确和一致的API,还要让你的代码具有很强的可重用性:
比如我们可以通过函数来封装一些API,通过传入不同的函数参数,让函数帮助我们完成不同的操作;
但是对于参数的类型是否也可以参数化呢?
什么是类型的参数化?
- 我们来提一个需求:封装一个函数,传入一个参数,并且返回这个参数;
如果我们是TypeScript的思维方式,要考虑这个参数和返回值的类型需要一致:
function foo(arg: number): number{
return arg
}
- 上面的代码虽然实现了,但是不适用于其他类型,比如string、boolean、Person等类型:
function foo(arg: string): string{
return arg
}
(2) 泛型实现类型参数化
虽然any是可以的,但是定义为any的时候,我们其实已经丢失了类型信息:
比如我们传入的是一个number,那么我们希望返回的可不是any类型,而是number类型;
所以,我们需要在函数中可以捕获到参数的类型是number,并且同时使用它来作为返回值的类型;
我们需要在这里使用一种特性的变量 - 类型变量(type variable),它作用于类型,而不是值:
function foo<Type>(arg: Type): Type{
return arg
}
这里我们可以使用两种方式来调用它:
方式一:通过 <类型> 的方式将类型传递给函数;
方式二:通过类型推到,自动推到出我们传入变量的类型:
- 在这里会推导出它们是 字面量类型的,因为字面量类型对于我们的函数也是适用的
foo<string>('coderking') -> foo('coderking')
foo<number>(666) -> foo(666)
(3) 泛型的基本补充
- 我们也可以传入多个类型
function foo<T,E>(a1: T, a1: E){
}
平时在开发中我们可能会看到一些常用的名称:
T:
Type的缩写,类型K、V:
key和value的缩写,键值对E:
Element的缩写,元素O:
Object的缩写,对象
(4) 泛型接口
- 在定义接口的时候我们也可以使用泛型
interface IPerson<T1 = string, T2 = number> {
name: T1
age: T2
}
const p: IPerson<string, string> = {
name: "CoderKing",
age: "18"
}
(5) 泛型类
- 泛型类的编写
class Point<T> {
x: T
y: T
z: T
constructor(x: T, y: T, z: T) {
this.x = x
this.y = y
this.z = y
}
}
const p1 = new Point("1.33.2", "2.22.3", "4.22.1")
const p2 = new Point<string>("1.33.2", "2.22.3", "4.22.1")
const p3: Point<string> = new Point("1.33.2", "2.22.3", "4.22.1")
const names1: string[] = ["abc", "cba", "nba"]
const names2: Array<string> = ["abc", "cba", "nba"] // 不推荐(react jsx <>)
(6) 泛型约束
有时候我们希望传入的类型有某些共性,但是这些共性可能不是在同一种类型中:
比如string和array都是有length的,或者某些对象也是会有length属性的;
那么只要是拥有length的属性都可以作为我们的参数类型,那么应该如何操作呢?
interface ILength {
length: number;
}
function getLength<T extends ILength>(arg: T) {
return arg.length;
}
getLength("abc");
getLength(["abc", "cba"]);
getLength({ length: 100 });
