TypeScript 中类、接口、枚举、函数、命名空间与模块的理解?应用场景?

类、接口、枚举定义

1、(Class)是面向对象程序设计(OOP,Object-Oriented Programming)实现信息封装的基础。类是一种用户定义的引用数据类型,也称类类型

2、接口是一系列抽象方法的声明,是一些方法特征的集合,这些方法都应该是抽象的,需要由具体的去实现,然后第三方就可以通过这组抽象方法调用,让具体的类执行具体的方法。简单来讲,一个接口所描述的是一个对象相关的属性和方法,但并不提供具体创建此对象实例的方法

3、枚举是一个被命名的整型常数的集合,用于声明一组命名的常数,当一个变量有几种可能的取值时,可以将它定义为枚举类型
通俗来说,枚举就是一个对象的所有可能取值的集合。在日常生活中也很常见,例如表示星期的SUNDAY、MONDAY、TUESDAY、WEDNESDAY、THURSDAY、FRIDAY、SATURDAY就可以看成是一个枚举

4、函数 JavaScript 应用程序的基础,帮助我们实现抽象层、模拟类、信息隐藏和模块
TypeScript 里,虽然已经支持类、命名空间和模块,但函数仍然是主要定义行为的方式,TypeScriptJavaScript 函数添加了额外的功能,丰富了更多的应用场景
函数类型在 TypeScript 类型系统中扮演着非常重要的角色,它们是可组合系统的核心构建块

使用方式

1. 类

定义类的关键字为 class,后面紧跟类名,类可以包含以下几个模块(类的数据成员):

  • 字段 : 字段是类里面声明的变量。字段表示对象的有关数据。
  • 构造函数: 类实例化时调用,可以为类的对象分配内存。
  • 方法: 方法为对象要执行的操作
class Car {
    // 字段
    engine:string;

    // 构造函数
    constructor(engine:string) {
        this.engine = engine
    }

    // 方法
    disp():void {
        console.log("发动机为 :   "+this.engine)
    }
}

继承

类的继承使用过extends的关键字

class Animal {
    move(distanceInMeters: number = 0) {
        console.log(`Animal moved ${distanceInMeters}m.`);
    }
}

class Dog extends Animal {
    bark() {
        console.log('Woof! Woof!');
    }
}

const dog = new Dog();
dog.bark();
dog.move(10);
dog.bark();

Dog是一个 派生类,它派生自 Animal 基类,派生类通常被称作子类,基类通常被称作 超类

Dog类继承了Animal类,因此实例dog也能够使用Animalmove方法

同样,类继承后,子类可以对父类的方法重新定义,这个过程称之为方法的重写,通过super关键字是对父类的直接引用,该关键字可以引用父类的属性和方法,如下:

class PrinterClass {
   doPrint():void {
      console.log("父类的 doPrint() 方法。")
   }
}

class StringPrinter extends PrinterClass {
   doPrint():void {
      super.doPrint() // 调用父类的函数
      console.log("子类的 doPrint()方法。")
   }
}

修饰符

可以看到,上述的形式跟ES6十分的相似,typescript在此基础上添加了三种修饰符:

  • 公共 public:可以自由的访问类程序里定义的成员
  • 私有 private:只能够在该类的内部进行访问
  • 受保护 protect:除了在该类的内部可以访问,还可以在子类中仍然可以访问

私有修饰符

只能够在该类的内部进行访问,实例对象并不能够访问

并且继承该类的子类并不能访问,如下图所示:

受保护修饰符

跟私有修饰符很相似,实例对象同样不能访问受保护的属性,如下:

有一点不同的是 protected 成员在子类中仍然可以访问

除了上述修饰符之外,还有只读修饰符

只读修饰符

通过readonly关键字进行声明,只读属性必须在声明时或构造函数里被初始化,如下:

除了实例属性之外,同样存在静态属性

静态属性

这些属性存在于类本身上面而不是类的实例上,通过static进行定义,访问这些属性需要通过 类型.静态属性 的这种形式访问,如下所示:

class Square {
    static width = '100px'
}

console.log(Square.width) // 100px

上述的类都能发现一个特点就是,都能够被实例化,在 typescript中,还存在一种抽象类

抽象类

抽象类做为其它派生类的基类使用,它们一般不会直接被实例化,不同于接口,抽象类可以包含成员的实现细节

abstract 关键字是用于定义抽象类和在抽象类内部定义抽象方法,如下所示:

abstract class Animal {
    abstract makeSound(): void;
    move(): void {
        console.log('roaming the earch...');
    }
}

这种类并不能被实例化,通常需要我们创建子类去继承,如下:

class Cat extends Animal {

    makeSound() {
        console.log('miao miao')
    }
}

const cat = new Cat()

cat.makeSound() // miao miao
cat.move() // roaming the earch..

接口 interface

接口定义如下:

interface interface_name {
}

例如有一个函数,这个函数接受一个 User 对象,然后返回这个 User 对象的 name 属性:

const getUserName = (user) => user.name

可以看到,参数需要有一个username属性,可以通过接口描述user参数的结构

interface User {
    name: string
    age: number
}

const getUserName = (user: User) => user.name

这些属性并不一定全部实现,上述传入的对象必须拥有nameage属性,否则typescript在编译阶段会报错,如下图:

如果不想要age属性的话,这时候可以采用可选属性,如下表示:

interface User {
    name: string
    age?: number
}

这时候age属性则可以是number类型或者undefined类型

有些时候,我们想要一个属性变成只读属性,在typescript只需要使用readonly声明,如下:

interface User {
    name: string
    age?: number
    readonly isMale: boolean
}

当我们修改属性的时候,就会出现警告,如下所示:

这是属性中有一个函数,可以如下表示:

interface User {
    name: string
    age?: number
    readonly isMale: boolean
    say: (words: string) => string
}

如果传递的对象不仅仅是上述的属性,这时候可以使用:

  • 类型推断
interface User {
    name: string
    age: number
}

const getUserName = (user: User) => user.name
getUserName({color: 'yellow'} as User)
  • 给接口添加字符串索引签名
interface User {
    name: string
    age: number
    [propName: string]: any;
}

接口还能实现继承,如下图:

也可以继承多个,父类通过逗号隔开,如下:

interface Father {
    color: String
}

interface Mother {
    height: Number
}

interface Son extends Father,Mother{
    name: string
    age: Number
}

枚举 enum

枚举的使用是通过enum关键字进行定义,形式如下:

enum xxx { ... }

声明关键字为枚举类型的方式如下:

// 声明d为枚举类型Direction
let d: Direction;

类型可以分成:

  • 数字枚举

  • 字符串枚举

  • 异构枚举

数字枚举

当我们声明一个枚举类型是,虽然没有给它们赋值,但是它们的值其实是默认的数字类型,而且默认从0开始依次累加:

enum Direction {
    Up,   // 值默认为 0
    Down, // 值默认为 1
    Left, // 值默认为 2
    Right // 值默认为 3
}

console.log(Direction.Up === 0); // true
console.log(Direction.Down === 1); // true
console.log(Direction.Left === 2); // true
console.log(Direction.Right === 3); // true

如果我们将第一个值进行赋值后,后面的值也会根据前一个值进行累加1:

enum Direction {
    Up = 10,
    Down,
    Left,
    Right
}

console.log(Direction.Up, Direction.Down, Direction.Left, Direction.Right); // 10 11 12 13

字符串枚举

枚举类型的值其实也可以是字符串类型:

enum Direction {
    Up = 'Up',
    Down = 'Down',
    Left = 'Left',
    Right = 'Right'
}

console.log(Direction['Right'], Direction.Up); // Right Up

如果设定了一个变量为字符串之后,后续的字段也需要赋值字符串,否则报错:

enum Direction {
 Up = 'UP',
 Down, // error TS1061: Enum member must have initializer
 Left, // error TS1061: Enum member must have initializer
 Right // error TS1061: Enum member must have initializer
}

异构枚举

即将数字枚举和字符串枚举结合起来混合起来使用,如下:

enum BooleanLikeHeterogeneousEnum {
    No = 0,
    Yes = "YES",
}

通常情况下我们很少会使用异构枚举

本质

现在一个枚举的案例如下:

enum Direction {
    Up,
    Down,
    Left,
    Right
}

通过编译后,javascript如下:

var Direction;
(function (Direction) {
    Direction[Direction["Up"] = 0] = "Up";
    Direction[Direction["Down"] = 1] = "Down";
    Direction[Direction["Left"] = 2] = "Left";
    Direction[Direction["Right"] = 3] = "Right";
})(Direction || (Direction = {}));

上述代码可以看到, Direction[Direction["Up"] = 0] = "Up"可以分成

  • Direction[“Up”] = 0
  • Direction[0] = “Up”

所以定义枚举类型后,可以通过正反映射拿到对应的值,如下:

enum Direction {
    Up,
    Down,
    Left,
    Right
}

console.log(Direction.Up === 0); // true
console.log(Direction[0]); // Up

并且多处定义的枚举是可以进行合并操作,如下:

enum Direction {
    Up = 'Up',
    Down = 'Down',
    Left = 'Left',
    Right = 'Right'
}

enum Direction {
    Center = 1
}

编译后,js代码如下:

var Direction;
(function (Direction) {
    Direction["Up"] = "Up";
    Direction["Down"] = "Down";
    Direction["Left"] = "Left";
    Direction["Right"] = "Right";
})(Direction || (Direction = {}));
(function (Direction) {
    Direction[Direction["Center"] = 1] = "Center";
})(Direction || (Direction = {}));

可以看到,Direction对象属性回叠加

函数function

javascript 定义函数十分相似,可以通过funciton 关键字、箭头函数等形式去定义,例如下面一个简单的加法函数:

const add = (a: number, b: number) => a + b

上述只定义了函数的两个参数类型,这个时候整个函数虽然没有被显式定义,但是实际上 TypeScript 编译器是能够通过类型推断到这个函数的类型,如下图所示:

当鼠标放置在第三行add函数名的时候,会出现完整的函数定义类型,通过: 的形式来定于参数类型,通过 => 连接参数和返回值类型

当我们没有提供函数实现的情况下,有两种声明函数类型的方式,如下所示:

// 方式一
type LongHand = {
  (a: number): number;
};

// 方式二
type ShortHand = (a: number) => number;

当存在函数重载时,只能使用方式一的形式

可选参数

当函数的参数可能是不存在的,只需要在参数后面加上 ? 代表参数可能不存在,如下:

const add = (a: number, b?: number) => a + (b ? b : 0)

这时候参数b可以是number类型或者undefined类型,即可以传一个number类型或者不传都可以

剩余类型

剩余参数与JavaScript的语法类似,需要用 ... 来表示剩余参数

如果剩余参数 rest 是一个由number类型组成的数组,则如下表示:

const add = (a: number, ...rest: number[]) => rest.reduce(((a, b) => a + b), a)

函数重载

允许创建数项名称相同但输入输出类型或个数不同的子程序,它可以简单地称为一个单独功能可以执行多项任务的能力

关于typescript函数重载,必须要把精确的定义放在前面,最后函数实现时,需要使用 |操作符或者?操作符,把所有可能的输入类型全部包含进去,用于具体实现

这里的函数重载也只是多个函数的声明,具体的逻辑还需要自己去写,typescript并不会真的将你的多个重名 function 的函数体进行合并

例如我们有一个add函数,它可以接收 string类型的参数进行拼接,也可以接收 number 类型的参数进行相加,如下:

// 上边是声明
function add (arg1: string, arg2: string): string
function add (arg1: number, arg2: number): number
// 因为我们在下边有具体函数的实现,所以这里并不需要添加 declare 关键字

// 下边是实现
function add (arg1: string | number, arg2: string | number) {
  // 在实现上我们要注意严格判断两个参数的类型是否相等,而不能简单的写一个 arg1 + arg2
  if (typeof arg1 === 'string' && typeof arg2 === 'string') {
    return arg1 + arg2
  } else if (typeof arg1 === 'number' && typeof arg2 === 'number') {
    return arg1 + arg2
  }
}

模块

TypeScript ECMAScript 2015 一样,任何包含顶级 import 或者 export 的文件都被当成一个模块

相反地,如果一个文件不带有顶级的import或者export声明,那么它的内容被视为全局可见的

例如我们在在一个 TypeScript 工程下建立一个文件 1.ts,声明一个变量a,如下:

const a = 1

然后在另一个文件同样声明一个变量a,这时候会出现错误信息

提示重复声明a变量,但是所处的空间是全局的

如果需要解决这个问题,则通过import或者export引入模块系统即可,如下:

const a = 10;

export default a

typescript中,export关键字可以导出变量或者类型,用法与es6模块一致,如下:

export const a = 1
export type Person = {
    name: String
}

通过import 引入模块,如下:

import { a, Person } from './export';

命名空间

命名空间一个最明确的目的就是解决重名问题

命名空间定义了标识符的可见范围,一个标识符可在多个名字空间中定义,它在不同名字空间中的含义是互不相干的

这样,在一个新的名字空间中可定义任何标识符,它们不会与任何已有的标识符发生冲突,因为已有的定义都处于其他名字空间中

TypeScript 中命名空间使用 namespace 来定义,语法格式如下:

namespace SomeNameSpaceName {
   export interface ISomeInterfaceName {      }
   export class SomeClassName {      }
}

以上定义了一个命名空间 SomeNameSpaceName,如果我们需要在外部可以调用 SomeNameSpaceName 中的类和接口,则需要在类和接口添加 export 关键字

使用方式如下:

SomeNameSpaceName.SomeClassName

命名空间本质上是一个对象,作用是将一系列相关的全局变量组织到一个对象的属性,如下:

namespace Letter {
  export let a = 1;
  export let b = 2;
  export let c = 3;
  // ...
  export let z = 26;
}

编译成js如下:

var Letter;
(function (Letter) {
    Letter.a = 1;
    Letter.b = 2;
    Letter.c = 3;
    // ...
    Letter.z = 26;
})(Letter || (Letter = {}));

命名空间与模块区别

  • 命名空间是位于全局命名空间下的一个普通的带有名字的 JavaScript 对象,使用起来十分容易。但就像其它的全局命名空间污染一样,它很难去识别组件之间的依赖关系,尤其是在大型的应用中

  • 像命名空间一样,模块可以包含代码和声明。 不同的是模块可以声明它的依赖

  • 在正常的TS项目开发过程中并不建议用命名空间,但通常在通过 d.ts 文件标记 js 库类型的时候使用命名空间,主要作用是给编译器编写代码的时候参考使用

应用场景

类class

除了日常借助类的特性完成日常业务代码,还可以将类(class)也可以作为接口,尤其在 React 工程中是很常用的,如下:

export default class Carousel extends React.Component<Props, State> {}

由于组件需要传入 props 的类型 Props ,同时有需要设置默认 propsdefaultProps,这时候更加适合使用class作为接口

先声明一个类,这个类包含组件 props 所需的类型和初始值:

// props的类型
export default class Props {
  public children: Array<React.ReactElement<any>> | React.ReactElement<any> | never[] = []
  public speed: number = 500
  public height: number = 160
  public animation: string = 'easeInOutQuad'
  public isAuto: boolean = true
  public autoPlayInterval: number = 4500
  public afterChange: () => {}
  public beforeChange: () => {}
  public selesctedColor: string
  public showDots: boolean = true
}

当我们需要传入 props 类型的时候直接将 Props 作为接口传入,此时 Props 的作用就是接口,而当需要我们设置defaultProps初始值的时候,我们只需要:

public static defaultProps = new Props()

Props 的实例就是 defaultProps 的初始值,这就是 class 作为接口的实际应用,我们用一个 class 起到了接口和设置初始值两个作用,方便统一管理,减少了代码量

接口 interface
例如在javascript中定义一个函数,用来获取用户的姓名和年龄:

const getUserInfo = function(user) {
    // ...
    return name: ${user.name}, age: ${user.age}
}

如果多人开发的都需要用到这个函数的时候,如果没有注释,则可能出现各种运行时的错误,这时候就可以使用接口定义参数变量:

// 先定义一个接口
interface IUser {
  name: string;
  age: number;
}

const getUserInfo = (user: IUser): string => {
  return `name: ${user.name}, age: ${user.age}`;
};

// 正确的调用
getUserInfo({name: "koala", age: 18});

包括后面讲到类的时候也会应用到接口

枚举 enum
就拿回生活的例子,后端返回的字段使用 0 - 6 标记对应的日期,这时候就可以使用枚举可提高代码可读性,如下:

enum Days {Sun, Mon, Tue, Wed, Thu, Fri, Sat};

console.log(Days["Sun"] === 0); // true
console.log(Days["Mon"] === 1); // true
console.log(Days["Tue"] === 2); // true
console.log(Days["Sat"] === 6); // true

包括后端日常返回0、1 等等状态的时候,我们都可以通过枚举去定义,这样可以提高代码的可读性,便于后续的维护

有需要的请私信博主,还请麻烦给个关注,博主不定期更新,或许能够有所帮助!!请关注公众号

在这里插入图片描述

  • 37
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

@逆风boy

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值