文章目录
- 类、接口、枚举定义
- 使用方式
- **1. 类**
- 继承
- 修饰符
- 私有修饰符
- 受保护修饰符
- 只读修饰符
- 静态属性
- 抽象类
- 接口 interface
- 枚举 enum
- 数字枚举
- 字符串枚举
- 异构枚举
- 本质
- 函数function
- 可选参数
- 剩余类型
- 函数重载
- 模块
- 命名空间
- 命名空间与模块区别
- 应用场景
- 有需要的请私信博主,还请麻烦给个关注,博主不定期更新,或许能够有所帮助!!请关注公众号
类、接口、枚举定义
1、类(Class)是面向对象程序设计(OOP,Object-Oriented Programming)实现信息封装的基础。类是一种用户定义的引用数据类型,也称类类型
2、接口是一系列抽象方法的声明,是一些方法特征的集合,这些方法都应该是抽象的,需要由具体的类去实现,然后第三方就可以通过这组抽象方法调用,让具体的类执行具体的方法。简单来讲,一个接口所描述的是一个对象相关的属性和方法,但并不提供具体创建此对象实例的方法
3、枚举是一个被命名的整型常数的集合,用于声明一组命名的常数,当一个变量有几种可能的取值时,可以将它定义为枚举类型
通俗来说,枚举就是一个对象的所有可能取值的集合。在日常生活中也很常见,例如表示星期的SUNDAY、MONDAY、TUESDAY、WEDNESDAY、THURSDAY、FRIDAY、SATURDAY就可以看成是一个枚举
4、函数是 JavaScript
应用程序的基础,帮助我们实现抽象层、模拟类、信息隐藏和模块
在 TypeScript
里,虽然已经支持类、命名空间和模块,但函数仍然是主要定义行为的方式,TypeScript
为 JavaScript
函数添加了额外的功能,丰富了更多的应用场景
函数类型在 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
也能够使用Animal
类move
方法
同样,类继承后,子类可以对父类的方法重新定义,这个过程称之为方法的重写,通过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
可以看到,参数需要有一个user
的name
属性,可以通过接口描述user
参数的结构
interface User {name: stringage: number
}const getUserName = (user: User) => user.name
这些属性并不一定全部实现,上述传入的对象必须拥有name
和age
属性,否则typescript
在编译阶段会报错,如下图:
如果不想要age
属性的话,这时候可以采用可选属性,如下表示:
interface User {name: stringage?: number
}
这时候age
属性则可以是number
类型或者undefined
类型
有些时候,我们想要一个属性变成只读属性,在typescript
只需要使用readonly
声明,如下:
interface User {name: stringage?: numberreadonly isMale: boolean
}
当我们修改属性的时候,就会出现警告,如下所示:
这是属性中有一个函数,可以如下表示:
interface User {name: stringage?: numberreadonly isMale: booleansay: (words: string) => string
}
如果传递的对象不仅仅是上述的属性,这时候可以使用:
- 类型推断
interface User {name: stringage: number
}const getUserName = (user: User) => user.name
getUserName({color: 'yellow'} as User)
- 给接口添加字符串索引签名
interface User {name: stringage: number[propName: string]: any;
}
接口还能实现继承,如下图:
也可以继承多个,父类通过逗号隔开,如下:
interface Father {color: String
}interface Mother {height: Number
}interface Son extends Father,Mother{name: stringage: Number
}
枚举 enum
枚举的使用是通过enum
关键字进行定义,形式如下:
enum xxx { ... }
声明关键字为枚举类型的方式如下:
// 声明d为枚举类型Direction
let d: Direction;
类型可以分成:
-
数字枚举
-
字符串枚举
-
异构枚举
数字枚举
当我们声明一个枚举类型是,虽然没有给它们赋值,但是它们的值其实是默认的数字类型,而且默认从0开始依次累加:
enum Direction {Up, // 值默认为 0Down, // 值默认为 1Left, // 值默认为 2Right // 值默认为 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 initializerLeft, // error TS1061: Enum member must have initializerRight // 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 + arg2if (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
,同时有需要设置默认 props
即 defaultProps
,这时候更加适合使用class
作为接口
先声明一个类,这个类包含组件 props
所需的类型和初始值:
// props的类型
export default class Props {public children: Array<React.ReactElement<any>> | React.ReactElement<any> | never[] = []public speed: number = 500public height: number = 160public animation: string = 'easeInOutQuad'public isAuto: boolean = truepublic autoPlayInterval: number = 4500public afterChange: () => {}public beforeChange: () => {}public selesctedColor: stringpublic 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 等等状态的时候,我们都可以通过枚举去定义,这样可以提高代码的可读性,便于后续的维护