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) interfacetype区别

  • 我们会发现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:keyvalue的缩写,键值对

    • 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 });