TypeScript技术知识整理

TypeScript技术知识整理

文章目录

  • TypeScript技术知识整理
    • 一、环境搭建与编译执行
      • 1.安装 `TypeScript` 编译器
      • 2.编写代码
        • **代码编辑器 - vscode**
      • 3.编译执行
      • 5.一些有用的编译选项
        • --outDir
        • --target
        • --watch
      • 6.编译配置文件
      • 7.指定加载的配置文件
    • 二、类型系统初识
      • 1.什么是类型
      • 2.动态类型语言 & 静态类型语言
        • **动态类型语言**
        • **静态类型语言**
        • 静态类型语言的优缺点
        • 动态类型语言的优缺点
      • 3.什么是类型系统
      • 4.类型标注
      • 5.类型检测
      • 6.类型标注的基本语法格式
      • 7.基础的简单的类型标注
      • 8.基础类型
      • 9.空和未定义类型
      • 10.对象类型
        • **内置对象类型**
        • **自定义对象类型**
        • **扩展**
      • 11.数组类型
        • **使用泛型标注**
        • **简单标注**
      • 12.元组类型
      • 13.枚举类型
        • **字符串类型枚举**
      • 14.无值类型
      • 15.Never类型
      • 16.任意类型
      • 17.未知类型
      • 18.函数类型
    • 三、接口
      • 1.接口定义
      • 2.可选属性
      • 3.只读属性
      • 4.任意属性
        • **数字类型索引**
        • **字符串类型索引**
      • 5.使用接口描述函数
      • 6.接口合并
    • 四、高级类型
      • 1.联合类型
      • 2.交叉类型
      • 3.字面量类型
      • 4.类型别名
        • 使用类型别名定义函数类型
        • interface 与 type 的区别
      • 5.类型推导
      • 6.类型断言
    • 五、函数详解
      • 1.函数的标注
      • 2.可选参数和默认参数
        • 可选参数
        • 默认参数
      • 3.剩余参数
      • 4.函数中的 this
        • 普通函数
        • 箭头函数
      • 5.函数重载
    • 六、面向对象编程
      • 1.类
      • 2.类的基础
      • 3.什么是类
      • 4.成员属性与方法定义
      • 5.构造函数
        • 构造函数参数属性
      • 6.继承
        • super 关键字
        • **方法重载**
      • 7.修饰符
        • public 修饰符
        • protected 修饰符
        • private 修饰符
        • readonly 修饰符
      • 8.寄存器
        • getter
        • setter
      • 9.静态成员
      • 10.抽象类
        • abstract 关键字
      • 11.类与接口
      • 12.类与对象类型
    • 七、泛型
      • 1.为什么要使用泛型
      • 2.泛型的使用
      • 3.泛型接口
        • **场景**
      • 4.泛型类
    • 八、装饰器
      • 1.什么是装饰器
      • 2.功能扩展
      • 3.装饰器语法
      • 4.装饰器
        • 类装饰器
        • 方法装饰器
        • 属性装饰器
        • 访问器装饰器
        • 参数装饰器
      • 5.装饰器执行顺序
      • 6.装饰器工厂
      • 7.元数据
        • 什么是元数据?
        • 使用 `reflect-metadata`
        • 定义元数据
        • 使用 `emitDecoratorMetadata`

一、环境搭建与编译执行

TypeScript 编写的程序并不能直接通过浏览器运行,我们需要先通过 TypeScript 编译器把 TypeScript 代码编译成 JavaScript 代码

TypeScript 的编译器是基于 Node.js 的,所以我们需要先安装 Node.js

1.安装 TypeScript 编译器

通过 NPM 包管理工具安装 TypeScript 编译器

npm i -g typescript

安装完成以后,我们可以通过命令 tsc 来调用编译器

# 查看当前 tsc 编译器版本
tsc -v

2.编写代码

代码编辑器 - vscode

vsCodeTypeScript都是微软的产品,vsCode 本身就是基于 TypeScript 进行开发的,vsCodeTypeScript 有着天然友好的支持

默认情况下,TypeScript 的文件的后缀为 .ts

TypeScript 代码

// ./src/helloWX.ts
let str: string = 'WST-WX';

3.编译执行

使用我们安装的 TypeScript 编译器 tsc.ts 文件进行编译

tsc ./src/helloKaiKeBa.ts

默认情况下会在当前文件所在目录下生成同名的 js 文件

5.一些有用的编译选项

编译命令 tsc 还支持许多编译选项,这里我先来了解几个比较常用的

–outDir

指定编译文件输出目录

tsc --outDir ./dist ./src/helloWX.ts

–target

指定编译的代码版本目标,默认为 ES3

tsc --outDir ./dist --target ES6 ./src/helloWX.ts

–watch

在监听模式下运行,当文件发生改变的时候自动编译

tsc --outDir ./dist --target ES6 --watch ./src/helloWX.ts

通过上面几个例子,我们基本可以了解 tsc 的使用了,但是大家应该也发现了,如果每次编译都输入这么一大堆的选项其实是很繁琐的,好在TypeScript 编译为我们提供了一个更加强大且方便的方式,编译配置文件:tsconfig.json,我们可以把上面的编译选项保存到这个配置文件中

6.编译配置文件

我们可以把编译的一些选项保存在一个指定的 json 文件中,默认情况下 tsc 命令运行的时候会自动去加载运行命令所在的目录下的 tsconfig.json 文件,配置文件格式如下

{
    // 推荐常用的配置格式
	"compilerOptions": {
		"outDir": "./dist", // 输出路径
		"target": "ES2015", // 定义编译后的ES语法
        "watch": true,      // 是否开启监听模式
        "removeComments": true, // 是否移除TS文件中的注释代码
        “noImplicitAny:true, // 是否允许any类型推断
        “strictNullChecks”: true, // 严格控制空值类型检查
        
	},
  // ** : 所有目录(包括子目录)
  // * : 所有文件,也可以指定类型 *.ts
  "include": ["./src/**/*"]
}

有了单独的配置文件,我们就可以直接运行

tsc

7.指定加载的配置文件

使用 --project-p 指定配置文件目录,会默认加载该目录下的 tsconfig.json 文件

tsc -p ./configs

也可以指定某个具体的配置文件

tsc -p ./configs/ts.json

二、类型系统初识

1.什么是类型

程序 = 数据结构 + 算法 = 各种格式的数据 + 处理数据的逻辑

数据是有格式(类型)的

  • 数字、布尔值、字符
  • 数组、集合

程序是可能有错误的

  • 计算错误(对非数字类型数据进行一些数学运算)
  • 调用一个不存在的方法

不同类型的数据有不同的操作方式或方法,如:字符串类型的数据就不应该直接参与数学运算

2.动态类型语言 & 静态类型语言

动态类型语言

程序运行期间才做数据类型检查的语言,如:JavaScript

静态类型语言

程序编译期间做数据类型检查的语言,如:Java

静态类型语言的优缺点

优点

  • 程序编译阶段(配合IDE、编辑器甚至可以在编码阶段)即可发现一些潜在错误,避免程序在生产环境运行了以后再出现错误
  • 编码规范、有利于团队开发协作、也更有利于大型项目开发、项目重构
  • 配合IDE、编辑器提供更强大的代码智能提示/检查
  • 代码即文档

缺点

  • 麻烦
  • 缺少灵活性

动态类型语言的优缺点

优点

  • 静态类型语言的缺点

缺点

  • 静态类型语言的优点

静态类型语言的核心 : 类型系统

3.什么是类型系统

类型系统包含两个重要组成部分

  • 类型标注(定义、注解) - typing
  • 类型检测(检查) - type-checking

4.类型标注

类型标注就是在代码中给数据(变量、函数(参数、返回值))添加类型说明,当一个变量或者函数(参数)等被标注以后就不能存储或传入与标注类型不符合的类型

有了标注,TypeScript 编译器就能按照标注对这些数据进行类型合法检测。

有了标注,各种编辑器、IDE等就能进行智能提示

5.类型检测

顾名思义,就是对数据的类型进行检测。注意这里,重点是类型两字。

类型系统检测的是类型,不是具体值(虽然,某些时候也可以检测值),比如某个参数的取值范围(1-100之间),我们不能依靠类型系统来完成这个检测,它应该是我们的业务层具体逻辑,类型系统检测的是它的值类型是否为数字!

6.类型标注的基本语法格式

TypeScript 中,类型标注的基本语法格式为:

数据载体:类型

TypeScript 的类型标注,我们可以分为

  • 基础的简单的类型标注
  • 高级的深入的类型标注

7.基础的简单的类型标注

  • 基础类型
  • 空和未定义类型
  • 对象类型
  • 数组类型
  • 元组类型
  • 枚举类型
  • 无值类型
  • Never类型
  • 任意类型
  • 未知类型(Version3.0 Added)

8.基础类型

基础类型包含:stringnumberboolean

标注语法

let title: string = 'ts';
let n: number = 100;
let isOk: boolean = true;

9.空和未定义类型

因为在 NullUndefined 这两种类型有且只有一个值,在标注一个变量为 NullUndefined 类型,那就表示该变量不能修改了

let a: null;
// ok
a = null;
// error
a = 1;

默认情况下 nullundefined 是所有类型的子类型。 就是说你可以把 nullundefined 其它类型的变量

let a: number;
// ok
a = null;

如果一个变量声明了,但是未赋值,那么该变量的值为 undefined,但是如果它同时也没有标注类型的话,默认类型为 anyany 类型后面有详细说明

// 类型为 `number`,值为 `undefined`
let a: number;
// 类型为 `any`,值为 `undefined`

小技巧

因为 nullundefined 都是其它类型的子类型,所以默认情况下会有一些隐藏的问题

let a:number;
a = null;
// ok(实际运行是有问题的)
a.toFixed(1);

小技巧:指定 strictNullChecks 配置为 true,可以有效的检测 null 或者 undefined,避免很多常见问题

let a:number;
a = null;
// error
a.toFixed(1);

也可以使我们程序编写更加严谨

let ele = document.querySelector('div');
// 获取元素的方法返回的类型可能会包含 null,所以最好是先进行必要的判断,再进行操作
if (ele) {
		ele.style.display = 'none';
}

10.对象类型

内置对象类型

JavaScript 中,有许多的内置对象,比如:Object、Array、Date……,我们可以通过对象的 构造函数 或者 来进行标注

let a: object = {};
// 数组这里标注格式有点不太一样,后面会在数组标注中进行详细讲解
let arr: Array = [1,2,3];
let d1: Date = new Date();


自定义对象类型

另外一种情况,许多时候,我们可能需要自定义结构的对象。这个时候,我们可以:

  • 字面量标注
  • 接口
  • 定义 或者 构造函数

字面量标注

let a: {username: string; age: number} = {
  username: 'zMouse',
  age: 35
};
// ok
a.username;
a.age;
// error
a.gender;


优点 : 方便、直接

缺点 : 不利于复用和维护

接口

// 这里使用了 interface 关键字,在后面的接口章节中会详细讲解
interface Person {
  username: string;
  age: number;
};
let a: Person = {
  username: 'WXin',
  age: 35
};
// ok
a.username;
a.age;
// error
a.gender;


优点 : 复用性高

缺点 : 接口只能作为类型标注使用,不能作为具体值,它只是一种抽象的结构定义,并不是实体,没有具体功能实现

类与构造函数

// 类的具体使用,也会在后面的章节中讲解
class Person {
	constructor(public username: string, public age: number) {
  }
}
// ok
a.username;
a.age;
// error
a.gender;


优点 : 功能相对强大,定义实体的同时也定义了对应的类型

缺点 : 复杂,比如只想约束某个函数接收的参数结构,没有必要去定一个类,使用接口会更加简单

interface AjaxOptions {
    url: string;
    method: string;
}

function ajax(options: AjaxOptions) {}

ajax({
    url: '',
    method: 'get'
});


扩展

包装对象:

这里说的包装对象其实就是 JavaScript 中的 StringNumberBoolean,我们知道 string 类型 和 String 类型并不一样,在 TypeScript 中也是一样

let a: string;
a = '1';
// error String有的,string不一定有(对象有的,基础类型不一定有)
a = new String('1');

let b: String;
b = new String('2');
// ok 和上面正好相反
b = '2';


11.数组类型

TypeScript 中数组存储的类型必须一致,所以在标注数组类型的时候,同时要标注数组中存储的数据类型

使用泛型标注

//  表示数组中存储的数据类型,泛型具体概念后续会讲
let arr1: Array = [];
// ok
arr1.push(100);
// error
arr1.push('haha');


简单标注

let arr2: string[] = [];
// ok
arr2.push('haha');
// error
arr2.push(1);


12.元组类型

元组类似数组,但是存储的元素类型不必相同,但是需要注意:

  • 初始化数据的个数以及对应位置标注类型必须一致
  • 越界数据必须是元组标注中的类型之一(标注越界数据可以不用对应顺序 - 联合类型
let data1: [string, number] = ['wangxin', 100];
// ok
data1.push(100);
// ok
data1.push('100');
// error
data1.push(true);


13.枚举类型

枚举的作用组织收集一组关联数据的方式,通过枚举我们可以给一组有关联意义的数据赋予一些友好的名字

enum HTTP_CODE {
  OK = 200,
  NOT_FOUND = 404,
  METHOD_NOT_ALLOWED
};
// 200
HTTP_CODE.OK;
// 405
HTTP_CODE.METHOD_NOT_ALLOWED;
// error
HTTP_CODE.OK = 1;


注意事项:

  • key 不能是数字
  • value 可以是数字,称为 数字类型枚举,也可以是字符串,称为 字符串类型枚举,但不能是其它值,默认为数字:0
  • 枚举值可以省略,如果省略,则:
    • 第一个枚举值默认为:0
    • 非第一个枚举值为上一个数字枚举值 + 1
  • 枚举值为只读(常量),初始化后不可修改

字符串类型枚举

枚举类型的值,也可以是字符串类型

enum URLS  {
  USER_REGISETER = '/user/register',
  USER_LOGIN = '/user/login',
  // 如果前一个枚举值类型为字符串,则后续枚举项必须手动赋值
  INDEX = 0
}


注意:如果前一个枚举值类型为字符串,则后续枚举项必须手动赋值

小技巧:枚举名称可以是大写,也可以是小写,推荐使用全大写(通常使用全大写的命名方式来标注值为常量)

14.无值类型

表示没有任何数据的类型,通常用于标注无返回值函数的返回值类型,函数默认标注类型为:void

function fn():void {
  	// 没有 return 或者 return undefined
}


strictNullChecksfalse 的情况下,undefinednull 都可以赋值给 void ,但是当 strictNullCheckstrue 的情况下,只有 undefined 才可以赋值给 void

15.Never类型

当一个函数永远不可能执行 return 的时候,返回的就是 never ,与 void 不同,void 是执行了 return, 只是没有值,never 是不会执行 return,比如抛出错误,导致函数终止执行

function fn(): never {
  	throw new Error('error');
}


16.任意类型

有的时候,我们并不确定这个值到底是什么类型或者不需要对该值进行类型检测,就可以标注为 any 类型

let a: any;


  • 一个变量申明未赋值且未标注类型的情况下,默认为 any 类型
  • 任何类型值都可以赋值给 any 类型
  • any 类型也可以赋值给任意类型
  • any 类型有任意属性和方法

注意:标注为 any 类型,也意味着放弃对该值的类型检测,同时放弃 IDE 的智能提示

小技巧:当指定 noImplicitAny 配置为 true,当函数参数出现隐含的 any 类型时报错

17.未知类型

unknow,3.0 版本中新增,属于安全版的 any,但是与 any 不同的是:

  • unknow 仅能赋值给 unknowany
  • unknow 没有任何属性和方法

18.函数类型

JavaScript 函数是非常重要的,在 TypeScript 也是如此。同样的,函数也有自己的类型标注格式

  • 参数
  • 返回值
函数名称( 参数1: 类型, 参数2: 类型... ): 返回值类型;


function add(x: number, y: number): number {
  	return x + y;
}


三、接口

1.接口定义

前面我们说到,TypeScript 的核心之一就是对值(数据)所具有的结构进行类型检查,除了一些前面说到基本类型标注,针对对象类型的数据,除了前面提到的一些方式以外,我们还可以通过: Interface (接口),来进行标注。

接口:对复杂的对象类型进行标注的一种方式,或者给其它代码定义一种契约(比如:类)

接口的基础语法定义结构特别简单

interface Point {
    x: number;
    y: number;
}

上面的代码定义了一个类型,该类型包含两个属性,一个 number 类型的 x 和一个 number 类型的 y,接口中多个属性之间可以使用 逗号 或者 分号 进行分隔

我们可以通过这个接口来给一个数据进行类型标注

let p1: Point = {
    x: 100,
    y: 100
};

注意:接口是一种 类型 ,不能作为 使用

interface Point {
    x: number;
    y: number;
}

let p1 = Point;	//错误

当然,接口的定义规则远远不止这些

2.可选属性

接口也可以定义可选的属性,通过 ? 来进行标注

interface Point {
    x: number;
    y: number;
    color?: string;
}

其中的 color? 表示该属性是可选的

3.只读属性

我们还可以通过 readonly 来标注属性为只读

interface Point {
    readonly x: number;
    readonly y: number;
}

当我们标注了一个属性为只读,那么该属性除了初始化以外,是不能被再次赋值的

4.任意属性

有的时候,我们希望给一个接口添加任意属性,可以通过索引类型来实现

数字类型索引

interface Point {
    x: number;
    y: number;
    [prop: number]: number;
}

字符串类型索引

interface Point {
    x: number;
    y: number;
    [prop: string]: number;
}

数字索引是字符串索引的子类型

注意:索引签名参数类型必须为 stringnumber 之一,但两者可同时出现

interface Point {
    [prop1: string]: string;
    [prop2: number]: string;
}

注意:当同时存在数字类型索引和字符串类型索引的时候,数字类型的值类型必须是字符串类型的值类型或子类型

interface Point1 {
    [prop1: string]: string;
    [prop2: number]: number;	// 错误
}
interface Point2 {
    [prop1: string]: Object;
    [prop2: number]: Date;	// 正确
}

5.使用接口描述函数

我们还可以使用接口来描述一个函数

interface IFunc {
  (a: string): string;
}

let fn: IFunc = function(a) {}

注意,如果使用接口来单独描述一个函数,是没 key

6.接口合并

多个同名的接口合并成一个接口

interface Box {
    height: number;
    width: number;
}

interface Box {
    scale: number;
}

let box: Box = {height: 5, width: 6, scale: 10}

  • 如果合并的接口存在同名的非函数成员,则必须保证他们类型一致,否则编译报错
  • 接口中的同名函数则是采用重载(在函数详解中会进一步讲解)

四、高级类型

1.联合类型

联合类型也可以称为多选类型,当我们希望标注一个变量为多个类型之一时可以选择联合类型标注, 的关系

function css(ele: Element, attr: string, value: string|number) {
    // ...
}

let box = document.querySelector('.box');
// document.querySelector 方法返回值就是一个联合类型
if (box) {
    // ts 会提示有 null 的可能性,加上判断更严谨
    css(box, 'width', '100px');
    css(box, 'opacity', 1);
    css(box, 'opacity', [1,2]);  // 错误
}

2.交叉类型

交叉类型也可以称为合并类型,可以把多种类型合并到一起成为一种新的类型,并且 的关系

对一个对象进行扩展:

interface o1 {x: number, y: string};
interface o2 {z: number};

let o: o1 & o2 = Object.assign({}, {x:1,y:'2'}, {z: 100});

小技巧

TypeScript 在编译过程中只会转换语法(比如扩展运算符,箭头函数等语法进行转换,对于 API 是不会进行转换的(也没必要转换,而是引入一些扩展库进行处理的),如果我们的代码中使用了 target 中没有的 API ,则需要手动进行引入,默认情况下 TypeScript 会根据 target 载入核心的类型库

targetes5 时: ["dom", "es5", "scripthost"]

targetes6 时: ["dom", "es6", "dom.iterable", "scripthost"]

如果代码中使用了这些默认载入库以外的代码,则可以通过 lib 选项来进行设置

http://www.typescriptlang.org/docs/handbook/compiler-options.html

3.字面量类型

有的时候,我们希望标注的不是某个类型,而是一个固定值,就可以使用字面量类型,配合联合类型会更有用

function setPosition(ele: Element, direction: 'left' | 'top' | 'right' | 'bottom') {
  	// ...
}

// ok
box && setPosition(box, 'bottom');
// error
box && setPosition(box, 'hehe');

4.类型别名

有的时候类型标注比较复杂,这个时候我们可以类型标注起一个相对简单的名字

type dir = 'left' | 'top' | 'right' | 'bottom';
function setPosition(ele: Element, direction: dir) {
  	// ...
}

使用类型别名定义函数类型

这里需要注意一下,如果使用 type 来定义函数类型,和接口有点不太相同

type callback = (a: string) => string;
let fn: callback = function(a) {};

// 或者直接
let fn: (a: string) => string = function(a) {}

interface 与 type 的区别

interface

  • 只能描述 object/class/function 的类型
  • 同名 interface 自动合并,利于扩展

type

  • 不能重名
  • 能描述所有数据

5.类型推导

每次都显式标注类型会比较麻烦,TypeScript 提供了一种更加方便的特性:类型推导。TypeScript 编译器会根据当前上下文自动的推导出对应的类型标注,这个过程发生在:

  • 初始化变量
  • 设置函数默认参数值
  • 返回函数值
// 自动推断 x 为 number
let x = 1;
// 不能将类型“"a"”分配给类型“number”
x = 'a';

// 函数参数类型、函数返回值会根据对应的默认值和返回值进行自动推断
function fn(a = 1) {return a * a}

6.类型断言

有的时候,我们可能标注一个更加精确的类型(缩小类型标注范围),比如:

let img = document.querySelector('#img');

我们可以看到 img 的类型为 Element,而 Element 类型其实只是元素类型的通用类型,如果我们去访问 src 这个属性是有问题的,我们需要把它的类型标注得更为精确:HTMLImageElement 类型,这个时候,我们就可以使用类型断言,它类似于一种 类型转换:

let img = document.querySelector('#img');

或者

let img = document.querySelector('#img') as HTMLImageElement;


注意:断言只是一种预判,并不会数据本身产生实际的作用,即:类似转换,但并非真的转换了

五、函数详解

1.函数的标注

一个函数的标注包含

  • 参数
  • 返回值
function fn(a: string): string {};
let fn: (a: string) => string = function(a) {};

type callback = (a: string): string;
interface ICallBack {
  (a: string): string;
}

let fn: callback = function(a) {};
let fn: ICallBack = function(a) {};

2.可选参数和默认参数

可选参数

通过参数名后面添加 ? 来标注该参数是可选的

let div = document.querySelector('div');
function css(el: HTMLElement, attr: string, val?: any) {

}
// 设置
div && css( div, 'width', '100px' );
// 获取
div && css( div, 'width' );

默认参数

我们还可以给参数设置默认值

  • 有默认值的参数也是可选的
  • 设置了默认值的参数可以根据值自动推导类型
function sort(items: Array, order = 'desc') {}
sort([1,2,3]);

// 也可以通过联合类型来限制取值
function sort(items: Array, order:'desc'|'asc' = 'desc') {}
// ok
sort([1,2,3]);
// ok
sort([1,2,3], 'asc');
// error
sort([1,2,3], 'abc');

3.剩余参数

剩余参数是一个数组,所以标注的时候一定要注意

interface IObj {
    [key:string]: any;
}
function merge(target: IObj, ...others: Array) {
    return others.reduce( (prev, currnet) => {
        prev = Object.assign(prev, currnet);
        return prev;
    }, target );
}
let newObj = merge({x: 1}, {y: 2}, {z: 3});

4.函数中的 this

无论是 JavaScript 还是 TypeScript ,函数中的 this 都是我们需要关心的,那函数中 this 的类型该如何进行标注呢?

  • 普通函数
  • 箭头函数

普通函数

对于普通函数而言,this 是会随着调用环境的变化而变化的,所以默认情况下,普通函数中的 this 被标注为 any,但我们可以在函数的第一个参数位(它不占据实际参数位置)上显式的标注 this 的类型

interface T {
    a: number;
    fn: (x: number) => void;
}

let obj1:T = {
    a: 1,
    fn(x: number) {
        //any类型
        console.log(this);
    }
}


let obj2:T = {
    a: 1,
    fn(this: T, x: number) {
        //通过第一个参数位标注 this 的类型,它对实际参数不会有影响
        console.log(this);
    }
}
obj2.fn(1);

箭头函数

箭头函数的 this 不能像普通函数那样进行标注,它的 this 标注类型取决于它所在的作用域 this 的标注类型

interface T {
    a: number;
    fn: (x: number) => void;
}

let obj2: T = {
    a: 2,
    fn(this: T) {
        return () => {
            // T
            console.log(this);
        }
    }
}

5.函数重载

有的时候,同一个函数会接收不同类型的参数返回不同类型的返回值,我们可以使用函数重载来实现,通过下面的例子来体会一下函数重载

function showOrHide(ele: HTMLElement, attr: string, value: 'block'|'none'|number) {
	//
}

let div = document.querySelector('div');

if (div) {
  showOrHide( div, 'display', 'none' );
  showOrHide( div, 'opacity', 1 );
	// error,这里是有问题的,虽然通过联合类型能够处理同时接收不同类型的参数,但是多个参数之间是一种组合的模式,我们需要的应该是一种对应的关系
  showOrHide( div, 'display', 1 );
}

我们来看一下函数重载

function showOrHide(ele: HTMLElement, attr: 'display', value: 'block'|'none');
function showOrHide(ele: HTMLElement, attr: 'opacity', value: number);
function showOrHide(ele: HTMLElement, attr: string, value: any) {
  ele.style[attr] = value;
}

let div = document.querySelector('div');

if (div) {
  showOrHide( div, 'display', 'none' );
  showOrHide( div, 'opacity', 1 );
  // 通过函数重载可以设置不同的参数对应关系
  showOrHide( div, 'display', 1 );
}

  • 重载函数类型只需要定义结构,不需要实体,类似接口
interface PlainObject {
    [key: string]: string|number;
}

function css(ele: HTMLElement, attr: PlainObject);
function css(ele: HTMLElement, attr: string, value: string|number);
function css(ele: HTMLElement, attr: any, value?: any) {
    if (typeof attr === 'string' && value) {
        ele.style[attr] = value;
    }
    if (typeof attr === 'object') {
        for (let key in attr) {
            ele.style[attr] = attr[key];
        }
    }
}

let div = document.querySelector('div');
if (div) {
    css(div, 'width', '100px');
    css(div, {
        width: '100px'
    });

    // error,如果不使用重载,这里就会有问题了
    css(div, 'width');
}


六、面向对象编程

1.类

classECMAScript6 中新增的语法,用于定义一个 ,在 TypeScript 中也有,并且有更多特性

2.类的基础

ECMAScript 中的类语法结构基本类似

  • class 关键字
  • 构造函数:constructor
  • 成员属性定义
  • 成员方法
  • this关键字

我们重点要说的是与 ECMAScript 中不同的点

3.什么是类

对象 : 对某种事物所拥有的特征和行为进行的一种结构化描述

interface User {
  id: number;
  username: string;
  password: string;
  postArticle(title: string, content: string): void;
}
// 通过 key / value 结构描述一个对象(一个用户)
let user1 = {
  id: 1,
  username: 'wXin',
  password: '123456',
  postArticle(title: string, content: string) {
    console.log(`${this.username} 发表了一篇文章: ${title}`)
  }
}
let user2 = {
  id: 2,
  username: 'WxIN2',
  password: '654321',
  postArticle(title: string, content: string) {
    console.log(`${this.username} 发表了一篇文章: ${title}`)
  }
}

: 对一类具有相同特性事物的抽象描述,通过 class 来描述一个类,组织类的结构

class User {
  
}

4.成员属性与方法定义

class User {
  id: number;
  username: string;
	password: string;
	
	postArticle(title: string, content: string): void {
    // 在类的内部可以通过 `this` 来访问成员属性和方法
    console.log(`${this.username} 发表了一篇文章: ${title}`)
  }
}

ECMAScript7 之前,类的成员属性是在构造函数中进行初始化的

5.构造函数

通过 new 运算符 + 类名,可以创建一个该类所描述的对象,我们称这个过程为:实例化

let user1 = new User;
let user2 = new User;

当我们 new User 的时候,会自动调用该类下的一个名为 constructor 的方法,如果没有显式定义该方法,则会自动创建一个无参的 constructor 的空方法

class User {
	
	constructor() {}
  
}

注意:构造函数 constructor 不允许有返回值类型标注

class User {
  id: number;
  username: string;
	password: string;
  
  constructor(id: number, username: string, password: string) {
    this.id = id;
    this.username = username;
    this.password = password;
  }
	
	postArticle(title: string, content: string): void {
    console.log(`${this.username} 发表了一篇文章: ${title}`)
  }
}

let user1 = new User(1, 'wXin', '123456');
let user2 = new User(2, 'WxIN2', '654321');

构造函数参数属性

我们可以给构造函数参数添加修饰符来直接生成成员属性

class User {
  
  constructor(
  	public id: number,
    public username: string,
    public password: string
  ) {
    // 可以省略初始化赋值
  }
	
	postArticle(title: string, content: string): void {
    console.log(`${this.username} 发表了一篇文章: ${title}`)
  }
}

let user1 = new User(1, 'wXin', '123456');
let user2 = new User(2, 'WxIN2', '654321');

6.继承

我们可以通过 extends 关键字来实现类的继承

class VIP extends User {
  
}

super 关键字

在子类中,我们可以通过 super 来引用父类

  • 如果子类有自己的构造函数,则需要在子类构造函数中显示的调用父类构造函数 : super(//参数),否则会报错
  • 在子类构造函数中只有在 super(//参数) 之后才能访问 this
  • 如果子类没有重写构造函数,则会在默认的 constructor 中无参调用 super()
  • 在子类中,可以通过 super 来访问父类的成员属性和方法
  • 通过 super 访问父类的的同时,会自动绑定上下文对象为当前子类 this
class VIP extends User {
  
  constructor(
  		id: number,
      username: string,
      password: string,
      public allowFileTypes = ['png','gif','jpg']
    ) {
        super(id, username, password);
    }
  
  postAttachment(file: File): void {
    console.log(`${this.username} 上传了一个附件: ${file.name}`)
  }
}

let vip1 = new VIP(1, 'wXin', '123456');
let fileElement = document.querySelector('input[type="file"]');
let file = fileElement.files && fileElement.files[0];
file && vip1.postAttachment(file);


方法重载

class VIP extends User {
  
    constructor(
  		id: number,
      username: string,
      password: string,
      public allowFileTypes = ['png','gif','jpg']
    ) {
        super(id, username, password);
    }
    
  	// postArticle 方法重载
    postArticle(title: string, content: string, file?: File): void {
      	// 通过 super 调用父类实例方法
        super.postArticle(title, content);
        file && this.postAttachment(file);
    }
    
    postAttachment(file: File): void {
        console.log(`${this.username} 上传了一个附件: ${file.name}`)
    }
}

// 具体使用场景
let vip1 = new VIP(1, 'wXin', '123456');
let fileElement = document.querySelector('input[type="file"]') as HTMLInputElement;
let buttonElement = document.querySelector('button') as HTMLButtonElement;

buttonElement.onclick = function() {
    // vip1.postArticle('标题一', '内容一');
    let file;
    if (fileElement.files) {
        file = fileElement.files[0];
        // vip1.postAttachment(file);
    }
    vip1.postArticle('标题一', '内容一', file);
}


7.修饰符

有的时候,我们希望对类成员(属性、方法)进行一定的访问控制,来保证数据的安全,通过 类修饰符 可以做到这一点,目前 TypeScript 提供了四种修饰符:

  • public:公有,默认
  • protected:受保护
  • private:私有
  • readonly:只读

public 修饰符

这个是类成员的默认修饰符,它的访问级别为:

  • 自身
  • 子类
  • 类外

protected 修饰符

它的访问级别为:

  • 自身
  • 子类

private 修饰符

它的访问级别为:

  • 自身

readonly 修饰符

只读修饰符只能针对成员属性使用,且必须在声明时或构造函数里被初始化,它的访问级别为:

  • 自身
  • 子类
  • 类外
class User {
  
  constructor(
  	// 可以访问,但是一旦确定不能修改
  	readonly id: number,
    // 可以访问,但是不能外部修改
    protected username: string,
    // 外部包括子类不能访问,也不可修改
    private password: string
  ) {
    // ...
  }
	// ...
}

let user1 = new User(1, 'wXin', '123456');


8.寄存器

有的时候,我们需要对类成员 属性 进行更加细腻的控制,就可以使用 寄存器 来完成这个需求,通过 寄存器,我们可以对类成员属性的访问进行拦截并加以控制,更好的控制成员属性的设置和访问边界,寄存器分为两种:

  • getter
  • setter

getter

访问控制器,当访问指定成员属性时调用

setter

设置控制器,当设置指定成员属性时调用

class User {
    private _id: number;
    private _username: string;
    private _password: string;
    
    constructor(id: number, username: string, password: string) {
        this.id = id;
        this.username = username;
        this.password = password;
    }

    public set id(id: number) {
        this._id = id;
    }

    public get id() {
        return this._id;
    }

    public set username(username: string) {
        this._username = username;
    }

    public get username() {
        return this._username;
    }

    public set password(password: string) {
        if (password.length >= 6) {
            this._password = password;
        }
    }

    public get password() {
        return '******';
    }
  	// ...
}


9.静态成员

前面我们说到的是成员属性和方法都是实例对象的,但是有的时候,我们需要给类本身添加成员

type allow_file_type_list = 'png'|'gif'|'jpg'|'jpeg'|'webp';

class VIP extends User {
  
  // static 必须再 readonly 之前
  static readonly ALLOW_FILE_TYPE_LIST: Array = ['png','gif','jpg','jpeg','webp'];
  
  private _allowFileTypes: Array;
  
  constructor(
  	  id: number,
      username: string,
      password: string,
      allowFileTypes: Array = ['png','gif','jpg']
    ) {
        super(id, username, password);
        this._allowFileTypes = allowFileTypes;
    }
  
  public set allowFileTypes(types: Array) {
    this._allowFileTypes = types;
  }

  public get allowFileTypes() {
    return this._allowFileTypes;
  }

  public addType(type: allow_file_type_list) {
    this._allowFileTypes.push(type);
  }
}

let vip1 = new VIP(1, 'wXin', '123456', ['jpg','jpeg']);
// vip1
console.log(vip1.allowFileTypes);
let vip2 = new VIP(2, 'cc', '654321');
// vip1
console.log(vip2.allowFileTypes);
// 所有 VIP 可以设置的附件类型
console.log(VIP.ALLOW_FILE_TYPE_LIST);


  • 类的静态成员是属于类的,所以不能通过实例对象(包括 this)来进行访问,而是直接通过类名访问(不管是类内还是类外)
  • 静态成员也可以通过访问修饰符进行修饰
  • 静态成员属性一般约定(非规定)全大写

10.抽象类

有的时候,一个基类(父类)的一些方法无法确定具体的行为,而是由继承的子类去实现,看下面的例子,

现在要通过一个类来美化系统的 MessageBox,它包含了:alert、confirm 和 prompt,设计结构如下:

// MessageBox
class MessageBox {
    constructor(){}
    show(){}
    close(){}
    
    // 注意这里,对于alert、confirm 和 prompt,它们有自己不同的内容,所以MessageBox无法去确定setContent的具体行为
    setContent(content: string){}
}
// alert
class Alert extends MessageBox {
    constructor(){
        super()
    }
    // 重写
    setContent(content: string){
        // 内容+一个确定按钮
    }
}
// confirm
class Confirm extends MessageBox {
    constructor(){
        super()
    }
    // 重写
    setContent(content: string){
        // 内容+一个确定按钮+一个取消按钮
    }
}
//prompt
class Prompt extends MessageBox {
    constructor(){
        super()
    }
    // 重写
    setContent(content: string){
        // 一个输入框+一个确定按钮+一个取消按钮
    }
}


大家可以发现每个子类都重写了父类的 setContent 方法,父类的 setContent 方法并不需要去实现什么,这个时候我们可以抽象父类的 setContent 方法

abstract 关键字

如果一个方法没有具体的实现方法,则可以通过 abstract 关键字进行修饰

// MessageBox
abstract class MessageBox {
    constructor(){}
    show(){}
    close(){}
    
    // 注意这里,对于alert、confirm 和 prompt,它们有自己不同的内容,所以MessageBox无法去确定setContent的具体行为
    abstract setContent(content: string): void
}


使用抽象类有一个好处:

约定了所有继承子类的所必须实现的方法,使类的涉及更加的规范

这里需要注意:

  • abstract 修饰的方法不能有方法体
  • 如果一个类有抽象方法,那么该类也必须为抽象的
  • 如果一个类是抽象的,那么就不能使用 new 进行实例化(因为抽象类表名该类有未实现的方法,所以不允许实例化)
  • 如果一个子类继承了一个抽象类,那么该子类就必须实现抽象类中的所有抽象方法,否则该类还得声明为抽象的

11.类与接口

在前面我们已经学习了接口的使用,通过接口,我们可以为对象定义一种结构和契约。我们还可以把接口与类进行结合,通过接口,让类去强制符合某种契约,从某个方面来说,当一个抽象类中只有抽象的时候,它就与接口没有太大区别了,但是 类会产生实体代码,接口不会

  • 一个类使用 implements 关键字来确定要实现的接口,当一个类 implements 了某个接口,那么该类必须实现接口中定义的结构
// 数据格式
interface SpreadSheetData {
    name: string;
    description: string;
}

// 定义一个SpreadSheet接口
interface SpreadSheetInfo {
    getInfo(): SpreadSheetData;
}

// 用户
class User implements SpreadSheetInfo {

    constructor(
        private id: number,
        private name: string,
        private gender: string
    ) {
        
    }

    getInfo() {
        return {
            name: this.name,
            description: `我叫 ${this.name},性别 ${this.gender}`
        }
    }
}

// 课目
class Course implements SpreadSheetInfo {

    constructor(
        private id: number,
        private type: string,
        private title: string,
        private price: number
    ) {

    }

    getInfo() {
        return {
            name: this.title,
            description: `${this.type} 新课程 ${this.title},只要 ${this.price}`
        }
    }

}

// 电子表格
class SpreadSheet {

    private _datas: Array

    public get datas() {
        return this._datas;
    }

    add(origin: SpreadSheetInfo) {
        this._datas.push( origin.getInfo() );
    }

}



let spreadSheet = new SpreadSheet();

let user1 = new User(1, 'Wxin', '男');
let user2 = new User(1, 'cc', '男');

let course1 = new Course(1, 'js', 'vue', 1000);
let course2 = new Course(2, 'js', 'react', 1000);

spreadSheet.add( user1 );
spreadSheet.add( user2 );
spreadSheet.add( course1 );
spreadSheet.add( course2 );


  • TypeScript 只支持单继承,不支持继承多个父类,而一个类可以实现多个接口,多个接口使用 , 分隔
interface SpreadSheetInfo {
    getInfo(): SpreadSheetData;
}
interface IStorage extends ILogger {
    save(data: string): void;
}


  • 接口也可以继承
interface SpreadSheetInfo {
    getInfo(): SpreadSheetData;
}
// IStorage
interface IStorage extends ILogger {
    save(data: string): void;
}


12.类与对象类型

当我们在 TypeScript 定义一个类的时候,其实同时定义了两个不同的类型

  • 类类型(构造函数类型)
  • 对象类型

首先,对象类型好理解,就是我们的 new 出来的实例类型

那类类型是什么,我们知道 JavaScript 中的类,或者说是 TypeScript 中的类其实本质上还是一个函数,当然我们也称为构造函数,那么这个类或者构造函数本身也是有类型的,那么这个类型就是类的类型

class Person {
	// 属于类的
  static type = '人';

  // 属于实例的
  name: string;
  age: number;
  gender: string;

  // 类的构造函数也是属于类的
  constructor( name: string, age: number, gender: '男'|'女' = '男' ) {
    this.name = name;
    this.age = age;
    this.gender = gender;
  }

}

let p1 = new Person('wx', 26, '男');

let Person2: typeof Person = Person;
console.log(Person2.type);


封装一个工厂函数

function createInstance(constructor: Person): Person {
  // 这是有错误的,因为 Person 表示的 new 出来的实例的类型,而不是构造函数(类)的类型
  return new constructor('wx', 26, '男');
}


正确的做法

interface PersonConstructor {
    new (name: string, age: number, gender: '男'|'女'): Person;
}
function createInstance(constructor: PersonConstructor): Person {
    return new constructor('wx', 26, '男');
}


或者

type PersonConstructor = typeof Person;
function createInstance(constructor: PersonConstructor): Person {
    return new constructor('wx', 26, '男');
}


注意上面的 typeof Person,我们就是通过 typeof 来获取这个类的类类型,这里的 typeofJavaScript 中的 typeof 有一定的差异性,后续我们会讲到

七、泛型

1.为什么要使用泛型

许多时候,标注的具体类型并不能确定,比如一个函数的参数类型

function sort(items: Array, order: 'desc'|'asc') {
    //...
}

上面的 sort 函数虽然标注了类型,但是同时也限制了传入的只能是字符串数组。实际上,我们是希望它能传入字符串数组、数字数组、甚至用户自定义的结构类型,这个时候我们希望传入的类型能在具体调用的时候再确定,就像是函数内部可变数据转为参数一样。泛型 - 就可以完成这个需求

2.泛型的使用

function sort(items: T, order: 'desc'|'asc'): T {
    //...
}

所谓的泛型,就是给可变(不定)的类型定义变量(参数),<> 类似 ()

3.泛型接口

我们还可以在接口中使用泛型

场景

后端提供了一些接口,用以返回一些数据,依据返回的数据格式定义如下接口:

interface IResponseData {
    code: number;
    message?: string;
    data: any;
}

我们会发现该接口的 data 项的具体格式不确定,不同的接口会返回的数据是不一样的

// 用户接口
interface IResponseUserData {
    id: number;
    username: string;
    email: string;
}
// 文章接口
interface IResponseArticleData {
    id: number;
    title: string;
    author: IResponseUserData;
}

这个时候我们可以对 IResponseData 使用泛型

interface IResponseData {
    code: number;
    message?: string;
    data: T;
}

下面是具体代码

function getUser(url: string) {
    return fetch(url).then(res => {
        return res.json();
    }).then( (data: IResponseData) => {
        return data;
    });
}
function getArticles(url: string) {
    return fetch(url).then(res => {
        return res.json();
    }).then( (data: IResponseData) => {
        return data;
    } )
}

~(async function(){
    let user = await getUser('');
    if (user.code === 1) {
        console.log(user.message);
    } else {
        console.log(user.data.username);
    }

    let articles = await getArticles('');
    if (articles.code === 1) {
        console.log(articles.message);
    } else {
        console.log(articles.data.id);
        console.log(articles.data.author.username);
    }
});

4.泛型类

还可以这类中使用泛型

class Queue {
    private items: Array = [];

    add(item: T) {
        this.items.push(item);
    }

    remove(): T | undefined {
        return this.items.shift();
    }
}

let q1 = new Queue();
q1.add('a');
q1.add('b');
let v = q1.remove();
if (v) {
    v.substring(0);
}

let q2 = new Queue();

let box = document.querySelector('.box');
let div = document.querySelector('div');
box && q2.add(box);
div && q2.add(div);
let v2 = q2.remove();
if (v2) {
    v2.classList.add('box')
}

八、装饰器

1.什么是装饰器

装饰器-DecoratorsTypeScript 中是一种可以在不修改类代码的基础上通过添加标注的方式来对类型进行扩展的一种方式

  • 提高代码复用率
  • 减少代码量
  • 提高代码扩展性、可读性和维护性

TypeScript 中,装饰器只能在类中使用

2.功能扩展

现有一个 M 类,实现了两个用于加、减的方法:addsub

// 原始类
class M {
    static add(a: number, b: number) {
        return a + b;
    }
    static sub(a: number, b: number) {
        return a - b;
    }
}

let v1 = M.add(1,2);
console.log(v1);
let v2 = M.sub(1,2);
console.log(v2);

需求:每次调用 addsub 的时候,同时保存(如:localStorage)参或者打印输出日志(如:console.log())与计算的数据与结果

// 原始类
class M {
    static add(a: number, b: number) {
      	let result = a + b;
      	log('add', a, b, result);
        return result;
    }
    static sub(a: number, b: number) {
				let result = a + b;
      	log('sub', a, b, result);
        return result;
    }
}

function storageData(type: string, a: number, b: number, result: number) {
  	console.log({
      type,
      a,
      b,
      result
    })
}

let v1 = M.add(1,2);
console.log(v1);
let v2 = M.sub(1,2);
console.log(v2);

上述方式虽然很快的实现了需求。但是,这样的做法对原有代码照成了破坏和侵入式的修改,不利于代码的维护。

storageData 抽离出来,通过 storageData 来包装方法

// 原始类
class M {
    static add(a: number, b: number) {
      	return a + b;
    }
    static sub(a: number, b: number) {
				return a - b;
    }
}

function storageData(fn: Function, type: string, a: number, b: number) {
  	let result = fn(a, b);
  	console.log({
      type,
      a,
      b,
      result
    })
  	return result;
}

let v1 = log(M.add, 'add', 1, 2);
console.log(v1);
let v2 = log(M.sub, 'sub', 1, 2);
console.log(v2);

这样做虽然可以避免去修改 addsub 方法,但是我们又得去大量的修改调用代码,怎样才能在不对代码进行修改也不对调用进行修改的同时来进行功能的扩展呢,这就是:装饰器 - Decorator

3.装饰器语法

装饰器的使用及其的简单

  • 装饰器本质就是一个函数,如上面的 storageData 就是
  • 通过特定语法在特定的位置调用装饰器函数即可对数据(类、方法、甚至参数等)进行扩展
// 装饰器函数
function log(target: Function, type: string, descriptor: PropertyDescriptor) {
    let value = descriptor.value;

    descriptor.value = function(a: number, b: number) {
        let result = value(a, b);
        console.log('日志:', {
            type,
            a,
            b,
            result
        })
        return result;
    }
}

// 原始类
class M {
    @log
    static add(a: number, b: number) {
        return a + b;
    }
    @log
    static sub(a: number, b: number) {
        return a - b;
    }
}

let v1 = M.add(1, 2);
console.log(v1);
let v2 = M.sub(1, 2);
console.log(v2);

4.装饰器

装饰器 是一个函数,它可以通过 @装饰器函数 这种特殊的语法附加在 方法访问符属性参数 上,对它们进行包装,然后返回一个包装后的目标对象(方法访问符属性参数 ),装饰器工作在类的构建阶段,而不是使用阶段

function 装饰器1() {}
...

@装饰器1
class MyClass {
  
  private _x: number;
  
  @装饰器2
  property1: number;
  
  @装饰器3
  get x() { return this._x; }
  
  @装饰器4
  public method1(@装饰器5 x: number) {
    //
  }
}

类装饰器

目标

  • 应用于类的构造函数

参数

  • 第一个参数(也只有一个参数)
    • 类的构造函数作为其唯一的参数

方法装饰器

目标

  • 应用于类的方法上

参数

  • 第一个参数
    • 静态方法:类的构造函数
    • 实例方法:类的原型对象
  • 第二个参数
    • 方法名称
  • 第三个参数
    • 方法描述符对象

属性装饰器

目标

  • 应用于类的属性上

参数

  • 第一个参数
    • 静态方法:类的构造函数
    • 实例方法:类的原型对象
  • 第二个参数
    • 属性名称

访问器装饰器

目标

  • 应用于类的访问器(getter、setter)上

参数

  • 第一个参数
    • 静态方法:类的构造函数
    • 实例方法:类的原型对象
  • 第二个参数
    • 属性名称
  • 第三个参数
    • 方法描述符对象

参数装饰器

目标

  • 应用在参数上

参数

  • 第一个参数
    • 静态方法:类的构造函数
    • 实例方法:类的原型对象
  • 第二个参数
    • 方法名称
  • 第三个参数
    • 参数在函数参数列表中的索引

5.装饰器执行顺序

实例装饰器

​ 属性 => 访问符 => 参数 => 方法

静态装饰器

​ 属性 => 访问符 => 参数 => 方法

​ 类

6.装饰器工厂

如果我们需要给装饰器执行过程中传入一些参数的时候,就可以使用装饰器工厂来实现

// 装饰器函数
function log(callback: Function) {
  	return function(target: Function, type: string, descriptor: PropertyDescriptor) {
     	 	let value = descriptor.value;

        descriptor.value = function(a: number, b: number) {
            let result = value(a, b);
            callback({
                type,
                a,
                b,
                result
            });
            return result;
        }
    }
}

// 原始类
class M {
    @log(function(result: any) {
      	console.log('日志:', result)
    })
    static add(a: number, b: number) {
        return a + b;
    }
    @log(function(result: any) {
      	localStorage.setItem('log', JSON.stringify(result));
    })
    static sub(a: number, b: number) {
        return a - b;
    }
}

let v1 = M.add(1, 2);
console.log(v1);
let v2 = M.sub(1, 2);
console.log(v2);

7.元数据

装饰器 函数中 ,我们可以拿到 方法访问符属性参数 的基本信息,如它们的名称,描述符 等,但是我们想获取更多信息就需要通过另外的方式来进行:元数据

什么是元数据?

元数据 :用来描述数据的数据,在我们的程序中,对象 等都是数据,它们描述了某种数据,另外还有一种数据,它可以用来描述 对象,这些用来描述数据的数据就是 元数据

比如一首歌曲本身就是一组数据,同时还有一组用来描述歌曲的歌手、格式、时长的数据,那么这组数据就是歌曲数据的元数据

使用 reflect-metadata

https://www.npmjs.com/package/reflect-metadata

首先,需要安装 reflect-metadata

npm install reflect-metadata

定义元数据

我们可以 方法 等数据定义元数据

  • 元数据会被附加到指定的 方法 等数据之上,但是又不会影响 方法 本身的代码

设置

Reflect.defineMetadata(metadataKey, metadataValue, target, propertyKey)

  • metadataKey:meta 数据的 key
  • metadataValue:meta 数据的 值
  • target:meta 数据附加的目标
  • propertyKey:对应的 property key

调用方式

  • 通过 Reflect.defineMetadata 方法调用来添加 元数据
  • 通过 @Reflect.metadata 装饰器来添加 元数据
@Reflect.metadata("name", '我是A类')
class A {
    @Reflect.metadata("name1", "val1")
    public method1() {
    }
   
  	@Reflect.metadata("name2", "val2")
  	public method2() {
    }
}

// or
Reflect.defineMetadata("name", "我是A类", A);
Reflect.defineMetadata("name1", "val1", new A, 'method1');
Reflect.defineMetadata("name2", "val2", new A, 'method2');

获取

Reflect.getMetadata(metadataKey, target, propertyKey)

参数的含义与 defineMetadata 对应

使用 emitDecoratorMetadata

tsconfig.json 中有一个配置 emitDecoratorMetadata,开启该特性,typescript 会在编译之后自动给 方法访问符属性参数 添加如下几个元数据

  • design:type:被装饰目标的类型
    • 成员属性:属性的标注类型
    • 成员方法:Function 类型
  • design:paramtypes
    • 成员方法:方法形参列表的标注类型
    • 类:构造函数形参列表的标注类型
  • design:returntype
    • 成员方法:函数返回值的标注类型
import "reflect-metadata"

function n(target: any) {
}
function f(name: string) {
    return function(target: any, propertyKey: string, descriptor: any) {
      	console.log( 'design type', Reflect.getMetadata('design:type', target, propertyKey) );
        console.log( 'params type', Reflect.getMetadata('design:paramtypes', target, propertyKey) );
        console.log( 'return type', Reflect.getMetadata('design:returntype', target, propertyKey) );
    }
}
function m(target: any, propertyKey: string) {

}

@n
class B {
    @m
    name: string;

    constructor(a: string) {

    }

    @f('')
    method1(a: string, b: string) {
        return 'a'
    }
}


编译后

__decorate([
    m,
    __metadata("design:type", String)
], B.prototype, "name", void 0);
__decorate([
    f(''),
    __metadata("design:type", Function),
    __metadata("design:paramtypes", [String, String]),
    __metadata("design:returntype", void 0)
], B.prototype, "method1", null);
B = __decorate([
    n,
    __metadata("design:paramtypes", [String])
], B);


你可能感兴趣的:(JavaScript进阶)