TS快速入门手册---车门焊死,谁也别想下车(万字长文 高能预警!!!!!)

TypeScript 初体验 - 环境搭建与编译执行

学习目标

  • 学会搭建 TypeScript 环境
  • 掌握 TypeScript 代码的编译与运行

环境搭建

TypeScript 编写的程序并不能直接经过浏览器运行,咱们须要先经过 TypeScript 编译器把 TypeScript 代码编译成 JavaScript 代码javascript

TypeScript 的编译器是基于 Node.js 的,因此咱们须要先安装 Node.jscss

安装 Node.js

nodejs.orghtml

安装完成之后,能够经过 终端 或者 cmd 等命令行工具来调用 node前端

# 查看当前 node 版本
node -v
复制代码

安装 TypeScript 编译器

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

npm i -g typescript
复制代码

安装完成之后,咱们能够经过命令 tsc 来调用编译器node

# 查看当前 tsc 编译器版本
tsc -v
复制代码

编写代码

代码编辑器 - vscodees6

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

code.visualstudio.com/ajax

TypeScript 文件算法

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

TypeScript 代码

// ./src/hello.ts
let str: string = 'Typescript';
复制代码

编译执行

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

tsc ./src/hello.ts
复制代码

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

一些有用的编译选项

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

--outDir

指定编译文件输出目录

tsc --outDir ./dist ./src/hello.ts
复制代码

--target

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

tsc --outDir ./dist --target ES6 ./src/hello.ts
复制代码

--watch

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

tsc --outDir ./dist --target ES6 --watch ./src/hello.ts
复制代码

经过上面几个例子,咱们基本能够了解 tsc 的使用了,可是你们应该也发现了,若是每次编译都输入这么一大堆的选项实际上是很繁琐的,好在TypeScript 编译为咱们提供了一个更增强大且方便的方式,编译配置文件:tsconfig.json,咱们能够把上面的编译选项保存到这个配置文件中

编译配置文件

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

{
	"compilerOptions": {
		"outDir": "./dist",
		"target": "ES2015",
    "watch": true,
	},
  // ** : 全部目录(包括子目录)
  // * : 全部文件,也能够指定类型 *.ts
  "include": ["./src/**/*"]
}
复制代码

有了单独的配置文件,咱们就能够直接运行

tsc
复制代码

指定加载的配置文件

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

tsc -p ./configs
复制代码

也能够指定某个具体的配置文件

tsc -p ./configs/ts.json
复制代码

类型系统初识

学习目标

  • 了解类型系统
    • 类型标注
    • 类型检测的好处
    • 使用场景
  • 掌握经常使用的类型标注的使用

什么是类型

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

数据是有格式(类型)的

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

程序是可能有错误的

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

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

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

动态类型语言

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

静态类型语言

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

静态类型语言的优缺点

优势

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

缺点

  • 麻烦
  • 缺乏灵活性

动态类型语言

优势

  • 静态类型语言的缺点

缺点

  • 静态类型语言的优势

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

什么是类型系统

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

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

类型标注

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

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

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

类型检测

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

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

类型标注

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

数据载体:类型
复制代码

TypeScript 的类型标注,咱们能够分为

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

基础的简单的类型标注

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

基础类型

基础类型包含:stringnumberboolean

标注语法

let title: string = '吧';
let n: number = 100;
let isOk: boolean = true;
复制代码

空和未定义类型

由于在 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';
}
复制代码

对象类型

内置对象类型

JavaScript 中,有许多的内置对象,好比:Object、Array、Date……,咱们能够经过对象的 构造函数 或者 来进行标注

let a: object = {};
// 数组这里标注格式有点不太同样,后面咱们在数组标注中进行详细讲解
let arr: Array<number> = [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: 'zMouse',
  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';

复制代码

数组类型

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

使用泛型标注

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

复制代码

简单标注

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

复制代码

元组类型

元组相似数组,可是存储的元素类型没必要相同,可是须要注意:

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

复制代码

枚举类型

枚举的做用组织收集一组关联数据的方式,经过枚举咱们能够给一组有关联意义的数据赋予一些友好的名字

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
}

复制代码

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

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

无值类型

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

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

复制代码

strictNullChecksfalse 的状况下,undefinednull 均可以赋值给 void ,可是当 strictNullCheckstrue 的状况下,只有 undefined 才能够赋值给 void

Never类型

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

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

复制代码

任意类型

有的时候,咱们并不肯定这个值究竟是什么类型或者不须要对该值进行类型检测,就能够标注为 any 类型

let a: any;

复制代码
  • 一个变量申明未赋值且未标注类型的状况下,默认为 any 类型
  • 任何类型值均可以赋值给 any 类型
  • any 类型也能够赋值给任意类型
  • any 类型有任意属性和方法

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

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

未知类型

unknow,3.0 版本中新增,属于安全版的 any,可是与 any 不一样的是:

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

函数类型

JavaScript 函数是很是重要的,在 TypeScript 也是如此。一样的,函数也有本身的类型标注格式

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

复制代码
function add(x: number, y: number): number {
  	return x + y;
}

复制代码

函数更多的细节内容,在后期有专门的章节来进行深刻的探讨

高级类型

学习目标

  • 使用 联合类型交叉类型字面量类型 来知足更多的标注需求
  • 使用 类型别名类型推导 简化标注操做
  • 掌握 类型断言 的使用

联合类型

联合类型也能够称为多选类型,当咱们但愿标注一个变量为多个类型之一时能够选择联合类型标注, 的关系

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]);  // 错误
}
复制代码

交叉类型

交叉类型也能够称为合并类型,能够把多种类型合并到一块儿成为一种新的类型,而且 的关系

对一个对象进行扩展:

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 选项来进行设置

www.typescriptlang.org/docs/handbo…

字面量类型

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

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

// ok
box && setDirection(box, 'bottom');
// error
box && setDirection(box, 'hehe');
复制代码

类型别名

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

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

  • 不能重名
  • 能描述全部数据

类型推导

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

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

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

类型断言

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

let img = document.querySelector('#img');
复制代码

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

let img = <HTMLImageElement>document.querySelector('#img');
复制代码

或者

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

复制代码

注意:断言只是一种预判,并不会数据自己产生实际的做用,即:相似转换,但并不是真的转换了

接口

学习目标

  • 理解接口的概念
  • 学会经过接口标注复杂结构的对象

接口定义

前面咱们说到,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;	//错误
复制代码

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

可选属性

接口也能够定义可选的属性,经过 ? 来进行标注

interface Point {
    x: number;
    y: number;
    color?: string;
}
复制代码

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

只读属性

咱们还能够经过 readonly 来标注属性为只读

interface Point {
    readonly x: number;
    readonly y: number;
}
复制代码

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

任意属性

有的时候,咱们但愿给一个接口添加任意属性,能够经过索引类型来实现

数字类型索引

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;	// 正确
}

复制代码

使用接口描述函数

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

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

let fn: IFunc = function(a) {}

复制代码

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

接口合并

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

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

interface Box {
    scale: number;
}

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

复制代码
  • 若是合并的接口存在同名的非函数成员,则必须保证他们类型一致,不然编译报错
  • 接口中的同名函数则是采用重载(具体后期函数详解中讲解)

函数详解

学习目标

  • 掌握 TypeScript 中的函数类型标注
  • 函数可选参数和参数默认值
  • 剩余参数
  • 函数中的 this
  • 函数重载

函数的标注

一个函数的标注包含

  • 参数
  • 返回值
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) {};
复制代码

可选参数和默认参数

可选参数

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

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<number>, order = 'desc') {}
sort([1,2,3]);

// 也能够经过联合类型来限制取值
function sort(items: Array<number>, order:'desc'|'asc' = 'desc') {}
// ok
sort([1,2,3]);
// ok
sort([1,2,3], 'asc');
// error
sort([1,2,3], 'abc');
复制代码

剩余参数

剩余参数是一个数组,因此标注的时候必定要注意

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

函数中的 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);
        }
    }
}
复制代码

函数重载

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

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

复制代码

面向对象编程

学习目标

  • 掌握面向对象编程中类的基本定义与语法
  • 学会使用类修饰符与寄存器
  • 理解并掌握类的实例成员与类的静态成员的区别与使用
  • 理解类与接口的关系,并熟练使用它们
  • 了解类(构造函数)类型与对象类型的区别

面向对象编程中一个重要的核心就是:,当咱们使用面向对象的方式进行编程的时候,一般会首先去分析具体要实现的功能,把特性类似的抽象成一个一个的类,而后经过这些类实例化出来的具体对象来完成具体业务需求。

类的基础

在类的基础中,包含下面几个核心的知识点,也是 TypeScriptEMCAScript2015+ 在类方面共有的一些特性

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

除了以上的共同特性之外,在 TypeScript 中还有许多 ECMAScript 没有的,或当前还不支持的一些特性,如:抽象

class

经过 class 就能够描述和组织一个类的结构,语法:

// 一般类的名称咱们会使用 大坨峰命名 规则,也就是 (单词)首字母大写
class User {
  // 类的特征都定义在 {} 内部
}
复制代码

构造函数

经过 class 定义了一个类之后,咱们能够经过 new 关键字来调用该类从而获得该类型的一个具体对象:也就是实例化。

为何类能够像函数同样去调用呢,其实咱们执行的并非这个类,而是类中包含的一个特殊函数:构造函数 - constructor

class User {
	
	constructor() {
    console.log('实例化...')
  }
  
}
let user1 = new User;
复制代码
  • 默认状况下,构造函数是一个空函数

  • 构造函数会在类被实例化的时候调用

  • 咱们定义的构造函数会覆盖默认构造函数

  • 若是在实例化(new)一个类的时候无需传入参数,则能够省略 ()

  • 构造函数 constructor 不容许有return 和返回值类型标注的(由于要返回实例对象)

一般状况下,咱们会把一个类实例化的时候的初始化相关代码写在构造函数中,好比对类成员属性的初始化赋值

成员属性与方法定义

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

let user1 = new User(1, 'zMouse');
let user2 = new User(2, 'MT');
复制代码

this 关键字

在类内部,咱们能够经过 this 关键字来访问类的成员属性和方法

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

构造函数参数属性

由于在构造函数中对类成员属性进行传参赋值初始化是一个比较常见的场景,因此 ts 提供了一个简化操做:给构造函数参数添加修饰符来直接生成成员属性

  • public 就是类的默认修饰符,表示该成员能够在任何地方进行读写操做
class User {
  
  constructor( public id: number, public username: string ) {
    // 能够省略初始化赋值
  }
	
	postArticle(title: string, content: string): void {
    console.log(`${this.username} 发表了一篇文章: ${title}`)
  }
}

let user1 = new User(1, 'zMouse');
let user2 = new User(2, 'MT');
复制代码

继承

ts 中,也是经过 extends 关键字来实现类的继承

class VIP extends User {
  
}
复制代码

super 关键字

在子类中,咱们能够经过 super 来引用父类

  • 若是子类没有重写构造函数,则会在默认的 constructor 中调用 super()

  • 若是子类有本身的构造函数,则须要在子类构造函数中显示的调用父类构造函数 : super(//参数),不然会报错

  • 在子类构造函数中只有在 super(//参数) 以后才能访问 this

  • 在子类中,能够经过 super 来访问父类的成员属性和方法

  • 经过 super 访问父类的的同时,会自动绑定上下文对象为当前子类 this

class VIP extends User {
  
  constructor( id: number, username: string, public score = 0 ) {
        super(id, username);
    }
  
  postAttachment(file: string): void {
    console.log(`${this.username} 上传了一个附件: ${file}`)
  }
}

let vip1 = new VIP(1, 'Leo');
vip1.postArticle('标题', '内容');
vip1.postAttachment('1.png');
复制代码

方法的重写与重载

默认状况下,子类成员方法集成自父类,可是子类也能够对它们进行重写和重载

class VIP extends User {
  
    constructor( id: number, username: string, public score = 0 ) {
        super(id, username);
    }
  
  	// postArticle 方法重写,覆盖
    postArticle(title: string, content: string): void {
      this.score++;
      console.log(`${this.username} 发表了一篇文章: ${title},积分:${this.score}`);
    }
    
    postAttachment(file: string): void {
        console.log(`${this.username} 上传了一个附件: ${file}`)
    }
}

// 具体使用场景
let vip1 = new VIP(1, 'Leo');
vip1.postArticle('标题', '内容');
复制代码
class VIP extends User {
  
    constructor( id: number, username: string, public score = 0 ) {
        super(id, username);
    }
  
    // 参数个数,参数类型不一样:重载
  	postArticle(title: string, content: string): void;
    postArticle(title: string, content: string, file: string): void;
    postArticle(title: string, content: string, file?: string) {
        super.postArticle(title, content);

        if (file) {
            this.postAttachment(file);
        }
    }
    
    postAttachment(file: string): void {
        console.log(`${this.username} 上传了一个附件: ${file}`)
    }
}

// 具体使用场景
let vip1 = new VIP(1, 'Leo');
vip1.postArticle('标题', '内容');
vip1.postArticle('标题', '内容', '1.png');

复制代码

修饰符

有的时候,咱们但愿对类成员(属性、方法)进行必定的访问控制,来保证数据的安全,经过 类修饰符 能够作到这一点,目前 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, 'zMouse', '123456');

复制代码

寄存器

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

  • getter
  • setter

getter

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

setter- 组件

- 函数式组件

- 类式组件

- props 与 state

- 组件通讯

- 表单与受控组件

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

class User {
    
    constructor( readonly _id: number, readonly _username: string, private _password: string ) {
    }

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

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

复制代码

静态成员

前面咱们说到的是成员属性和方法都是实例对象的,可是有的时候,咱们须要给类自己添加成员,区分某成员是静态仍是实例的:

  • 该成员属性或方法是类型的特征仍是实例化对象的特征
  • 若是一个成员方法中没有使用或依赖 this ,那么该方法就是静态的
type IAllowFileTypeList = 'png'|'gif'|'jpg'|'jpeg'|'webp';

class VIP extends User {
  
  // static 必须在 readonly 以前
  static readonly ALLOW_FILE_TYPE_LIST: Array<IAllowFileTypeList> = ['png','gif','jpg','jpeg','webp'];
  
  constructor( id: number, username: string, private _allowFileTypes: Array<IAllowFileTypeList> ) {
        super(id, username);
  }
  
  info(): void {
    // 类的静态成员都是使用 类名.静态成员 来访问
    // VIP 这种类型的用户容许上传的全部类型有哪一些
    console.log(VIP.ALLOW_FILE_TYPE_LIST);
    // 当前这个 vip 用户容许上传类型有哪一些
    console.log(this._allowFileTypes);
  }
}

let vip1 = new VIP(1, 'zMouse', ['jpg','jpeg']);
// 类的静态成员都是使用 类名.静态成员 来访问
console.log(VIP.ALLOW_FILE_TYPE_LIST);
this.info();

复制代码
  • 类的静态成员是属于类的,因此不能经过实例对象(包括 this)来进行访问,而是直接经过类名访问(无论是类内仍是类外)
  • 静态成员也能够经过访问修饰符进行修饰
  • 静态成员属性通常约定(非规定)全大写

抽象类

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

如今前端比较流行组件化设计,好比 React

class MyComponent extends Component {
  
  constructor(props) {
    super(props);
    this.state = {}
  }
  
  render() {
    //...
  }
  
}

复制代码

根据上面代码,咱们能够大体设计以下类结构

  • 每一个组件都一个 props 属性,能够经过构造函数进行初始化,由父级定义
  • 每一个组件都一个 state 属性,由父级定义
  • 每一个组件都必须有一个 render 的方法
class Component<T1, T2> {

    public state: T2;

    constructor( public props: T1 ) {
       	// ...
    }
  
  	render(): string {
      	// ...不知道作点啥才好,可是为了不子类没有 render 方法而致使组件解析错误,父类就用一个默认的 render 去处理可能会出现的错误
    }
}

interface IMyComponentProps {
    title: string;
}
interface IMyComponentState {
    val: number;
}
class MyComponent extends Component<IMyComponentProps, IMyComponentState> {

    constructor(props: IMyComponentProps) {
        super(props);

        this.state = {
            val: 1
        }
    }

    render() {
      	this.props.title;
        this.state.val;
        return `<div>组件</div>`;
    }

}

复制代码

上面的代码虽然从功能上讲没什么太大问题,可是咱们能够看到,父类的 render 有点尴尬,其实咱们更应该从代码层面上去约束子类必须得有 render 方法,不然编码就不能经过

abstract 关键字

若是一个方法没有具体的实现方法,则能够经过 abstract 关键字进行修饰

abstract class Component<T1, T2> {

    public state: T2;

    constructor(
        public props: T1
    ) {
    }

    public abstract render(): string;
}

复制代码

使用抽象类有一个好处:

约定了全部继承子类的所必须实现的方法,使类的设计更加的规范

使用注意事项:

  • abstract 修饰的方法不能有方法体
  • 若是一个类有抽象方法,那么该类也必须为抽象的
  • 若是一个类是抽象的,那么就不能使用 new 进行实例化(由于抽象类表名该类有未实现的方法,因此不容许实例化)
  • 若是一个子类继承了一个抽象类,那么该子类就必须实现抽象类中的全部抽象方法,不然该类还得声明为抽象的

类与接口

在前面咱们已经学习了接口的使用,经过接口,咱们能够为对象定义一种结构和契约。咱们还能够把接口与类进行结合,经过接口,让类去强制符合某种契约,从某个方面来讲,当一个抽象类中只有抽象的时候,它就与接口没有太大区别了,这个时候,咱们更推荐经过接口的方式来定义契约

  • 抽象类编译后仍是会产生实体代码,而接口不会
  • TypeScript 只支持单继承,即一个子类只能有一个父类,可是一个类能够实现过个接口
  • 接口不能有实现,抽象类能够

implements

在一个类中使用接口并非使用 extends 关键字,而是 implements

  • 与接口相似,若是一个类 implements 了一个接口,那么就必须实现该接口中定义的契约
  • 多个接口使用 , 分隔
  • implementsextends 可同时存在
interface ILog {
  getInfo(): string;
}

class MyComponent extends Component<IMyComponentProps, IMyComponentState> implements ILog {

    constructor(props: IMyComponentProps) {
        super(props);

        this.state = {
            val: 1
        }
    }

    render() {
      	this.props.title;
        this.state.val;
        return `<div>组件</div>`;
    }
  
  	getInfo() {
      	return `组件:MyComponent,props:${this.props},state:${this.state}`;
    }

}

复制代码

实现多个接口

interface ILog {
    getInfo(): string;
}
interface IStorage {
    save(data: string): void;
}

class MyComponent extends Component<IMyComponentProps, IMyComponentState> implements ILog, IStorage {

    constructor(props: IMyComponentProps) {
        super(props);

        this.state = {
            val: 1
        }
    }

    render() {
      	this.props.title;
        this.state.val;
        return `<div>组件</div>`;
    }
  
  	getInfo(): string {
      	return `组件:MyComponent,props:${this.props},state:${this.state}`;
    }
  
  	save(data: string) {
      	// ... 存储
    }

}

复制代码

接口也能够继承

interface ILog {
    getInfo(): string;
}
interface IStorage extends ILog {
    save(data: string): void;
}

复制代码

类与对象类型

当咱们在 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;
  }
  
  public eat(): void {
    // ...
  }

}

let p1 = new Person('zMouse', 35, '男');
p1.eat();
Person.type;

复制代码

上面例子中,有两个不一样的数据

  • Person 类(构造函数)
  • 经过 Person 实例化出来的对象 p1

对应的也有两种不一样的类型

  • 实例的类型(Person
  • 构造函数的类型(typeof Person

用接口的方式描述以下

interface Person {
    name: string;
    age: number;
    gender: string;
    eat(): void;
}

interface PersonConstructor {
  	// new 表示它是一个构造函数
    new (name: string, age: number, gender: '男'|'女'): PersonInstance;
    type: string;
}

复制代码

在使用的时候要格外注意

function fn1(arg: Person /*若是但愿这里传入的Person 的实例对象*/) {
  	arg.eat();
}
fn1( new Person('', 1, '男') );

function fn2(arg: typeof Person /*若是但愿传入的Person构造函数*/) {
  	new arg('', 1, '男');
}
fn2(Person);

复制代码

TypeScript 的模块系统

TS 模块系统

虽然早期的时候,TypeScript 有一套本身的模块系统实现,可是随着更新,以及 JavaScript 模块化的日趋成熟,TypeScriptESM 模块系统的支持也是愈来愈完善

模块

不管是 JavaScript 仍是 TypeScript 都是以一个文件做为模块最小单元

  • 任何一个包含了顶级 import 或者 export 的文件都被当成一个模块
  • 相反的一个文件不带有顶级的 import 或者 export ,那么它的内容就是全局可见的

全局模块

若是一个文件中没有顶级 import 或者 export ,那么它的内容就是全局的,整个项目可见的

// a.ts
let a1 = 100;
let a2 = 200;
复制代码
// b.ts
// ok, 100
console.log(a1);
// error
let a2 = 300;
复制代码

不推荐使用全局模块,由于它会容易形成代码命名冲突(全局变量污染)

文件模块

任何一个包含了顶级 import 或者 export 的文件都会当作一个模块,在 TypeScript 中也称为外部模块。

模块语法

TypeScriptESM 语法相似

导出模块内部数据

使用 export 导出模块内部数据(变量、函数、类、类型别名、接口……)

导入外部模块数据

使用 import 导入外部模块数据

模块编译

TypeScript 编译器也可以根据相应的编译参数,把代码编译成指定的模块系统使用的代码

module 选项

TypeScript 编译选项中,module 选项是用来指定生成哪一个模块系统的代码,可设置的值有:"none""commonjs""amd""udm""es6"/"es2015/esnext""System"

  • target=="es3" or "es5":默认使用 commonjs
  • 其它状况,默认 es6

模块导出默认值的问题

若是一个模块没有默认导出

// m1.ts
export let obj = {
  x: 1
}
复制代码

则在引入该模块的时候,须要使用下列一些方式来导入

// main.ts
// error: 提示 m1 模块没有默认导出
import v from './m1'

// 能够简单的使用以下方式
import {obj} from './m1'
console.log(obj.x)
// or
import * as m1 from './m1'
console.log(m1.obj.x)
复制代码

加载非 TS 文件

有的时候,咱们须要引入一些 js 的模块,好比导入一些第三方的使用 js 而非 ts 编写的模块,默认状况下 tsc 是不对非 ts 模块文件进行处理的

咱们能够经过 allowJs 选项开启该特性

// m1.js
export default 100;
// main.ts
import m1 from './m1.js'
复制代码

ESM 模块中的默认值问题

ESM 中模块能够设置默认导出值

export default 'hello';
复制代码

可是在 CommonJSAMD 中是没有默认值设置的,它们导出的是一个对象(exports

module.exports.obj = {
    x: 100
}
复制代码

TypeScript 中导入这种模块的时候会出现 模块没有默认导出的错误提示

简单一些的作法:

import * as m from './m1.js'
复制代码

经过配置选项解决:

allowSyntheticDefaultImports

设置为:true,容许从没有设置默认导出的模块中默认导入。

虽然经过上面的方式能够解决编译过程当中的检测问题,可是编译后的具体要运行代码仍是有问题的

esModuleInterop

设置为:true,则在编译的同时生成一个 __importDefault 函数,用来处理具体的 default 默认导出

注意:以上设置只能当 module 不为 es6+ 的状况下有效

以模块的方式加载 JSON 格式的文件

TypeScript 2.9+ 版本添加了一个新的编译选项:resolveJsonModule,它容许咱们把一个 JSON 文件做为模块进行加载

resolveJsonModule

设置为:true ,能够把 json 文件做为一个模块进行解析

data.json

{
    "name": "zMouse",
    "age": 35,
    "gender": "男"
}
复制代码

ts文件

import * as userData from './data.json';
console.log(userData.name);
复制代码

模块解析策略

什么是模块解析

模块解析是指编译器在查找导入模块内容时所遵循的流程。

相对与非相对模块导入

根据模块引用是相对的仍是非相对的,模块导入会以不一样的方式解析。

相对导入

相对导入是以 /./../ 开头的引用

// 导入根目录下的 m1 模块文件
import m1 from '/m1'
// 导入当前目录下的 mods 目录下的 m2 模块文件
import m2 from './mods/m2'
// 导入上级目录下的 m3 模块文件
import m3 from '../m3'
复制代码

非相对导入

全部其它形式的导入被看成非相对的

import m1 from 'm1'
复制代码

模块解析策略

为了兼容不一样的模块系统(CommonJSESM),TypeScript 支持两种不一样的模块解析策略:NodeClassic,当 --module 选项为:AMDSystemES2015 的时候,默认为 Classic ,其它状况为 Node

--moduleResolution 选项

除了根据 --module 选项自动选择默认模块系统类型,咱们还能够经过 --moduleResolution 选项来手动指定解析策略

// tsconfig.json
{
  ...,
  "moduleResolution": "node"
}
复制代码

Classic 模块解析策略

该策略是 TypeScript 之前的默认解析策略,它已经被新的 Node 策略所取代,如今使用该策略主要是为了向后兼容

相对导入

// /src/m1/a.ts
import b from './b.ts'
复制代码

解析查找流程:

  1. src/m1/b.ts

默认后缀补全

// /src/m1/a.ts
import b from './b'
复制代码

解析查找流程:

  1. /src/m1/b.ts

  2. /src/m1/b.d.ts

非相对导入

// /src/m1/a.ts
import b from 'b'
复制代码

对于非相对模块的导入,则会从包含导入文件的目录开始依次向上级目录遍历查找,直到根目录为止

  1. /src/m1/b.ts

  2. /src/m1/b.d.ts

  3. /src/b.ts

  4. /src/b.d.ts

  5. /b.ts

  6. /b.d.ts

Node 模块解析策略

该解析策略是参照了 Node.js 的模块解析机制

相对导入

// node.js
// /src/m1/a.js
import b from './b'
复制代码

Classic 中,模块只会按照单个的文件进行查找,可是在 Node.js 中,会首先按照单个文件进行查找,若是不存在,则会按照目录进行查找

  1. /src/m1/b.js
  2. /src/m1/b/package.json中'main'中指定的文件
  3. /src/m1/b/index.js

非相对导入

// node.js
// /src/m1/a.js
import b from 'b'
复制代码

对于非相对导入模块,解析是很特殊的,Node.js 会这一个特殊文件夹 node_modules 里查找,而且在查找过程当中从当前目录的 node_modules 目录下逐级向上级文件夹进行查找

  1. /src/m1/node_modules/b.js
  2. /src/m1/node_modules/b/package.json中'main'中指定的文件
  3. /src/m1/node_modules/b/index.js
  4. /src/node_modules/b.js
  5. /src/node_modules/b/package.json中'main'中指定的文件
  6. /src/node_modules/b/index.js
  7. /node_modules/b.js
  8. /node_modules/b/package.json中'main'中指定的文件
  9. /node_modules/b/index.js

TypeScript 模块解析策略

TypeScript 如今使用了与 Node.js 相似的模块解析策略,可是 TypeScript 增长了其它几个源文件扩展名的查找(.ts.tsx.d.ts),同时 TypeScriptpackage.json 里使用字段 types 来表示 main 的意义

命名空间

命名空间

TS 中,exportimport 称为 外部模块,TS 中还支持一种内部模块 namespace,它的主要做用只是单纯的在文件内部(模块内容)隔离做用域

namespace k1 {
    let a = 10;
    export var obj = {
        a
    }
}

namespace k2 {
    let a = 20;
    console.log(k1.obj);
}
复制代码

装饰器

学习目标

  • 了解装饰器语法,学会使用装饰器对类进行扩展
  • 清楚装饰器执行顺序
  • 了解元数据以及针对装饰器的元数据编程

什么是装饰器

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

  • 减小代码量
  • 提升代码扩展性、可读性和维护性

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

装饰器语法

装饰器的使用极其的简单

  • 装饰器本质就是一个函数
  • 经过特定语法在特定的位置调用装饰器函数便可对数据(类、方法、甚至参数等)进行扩展

启用装饰器特性

  • experimentalDecorators: true
// 装饰器函数
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);
复制代码

装饰器

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

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

@装饰器1
class MyClass {
  
  @装饰器2
  a: number;
  
  @装饰器3
  static property1: number;
  
  @装饰器4
  get b() { 
    return 1; 
  }
  
  @装饰器5
  static get c() {
    return 2;
  }
  
  @装饰器6
  public method1(@装饰器5 x: number) {
    //
  }
  
  @装饰器7
  public static method2() {}
}
复制代码

类装饰器

目标

  • 应用于类的构造函数

参数

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

方法装饰器

目标

  • 应用于类的方法上

参数

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

属性装饰器

目标

  • 应用于类的属性上

参数

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

访问器装饰器

目标

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

参数

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

参数装饰器

目标

  • 应用在参数上

参数

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

装饰器执行顺序

实例装饰器

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

静态装饰器

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

​ 类

装饰器工厂

若是咱们须要给装饰器执行过程当中传入一些参数的时候,就可使用装饰器工厂来实现

// 装饰器函数
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);
复制代码

元数据

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

什么是元数据?

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

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

使用 reflect-metadata

www.npmjs.com/package/ref…

首先,须要安装 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 装饰器来添加 元数据

import "reflect-metadata"

@Reflect.metadata("n", 1)
class A {
    @Reflect.metadata("n", 2)
    public static method1() {
    }
  
  	@Reflect.metadata("n", 4)
  	public method2() {
    }
}

// or
Reflect.defineMetadata('n', 1, A);
Reflect.defineMetadata('n', 2, A, 'method1');

let obj = new A();
Reflect.defineMetadata('n', 3, obj);
Reflect.defineMetadata('n', 4, obj, 'method2');

console.log(Reflect.getMetadata('n', A));
console.log(Reflect.getMetadata('n', A, ));
复制代码

获取

Reflect.getMetadata(metadataKey, target, propertyKey)

参数的含义与 defineMetadata 对应

使用元数据的 log 装饰器

import "reflect-metadata"

function L(type = 'log') {
  	return function(target: any) {
      	Reflect.defineMetadata("type", type, target);
    }
}
// 装饰器函数
function log(callback: Function) {
  	return function(target: any, name: string, descriptor: PropertyDescriptor) {
     	 	let value = descriptor.value;
      
      	let type = Reflect.getMetadata("type", target);

        descriptor.value = function(a: number, b: number) {
            let result = value(a, b);
          	if (type === 'log') {
              	console.log('日志:', {
                  name,
                  a,
                  b,
                  result
                })
            }
          	if (type === 'storage') {
                localStorage.setItem('storageLog', JSON.stringify({
                  name,
                  a,
                  b,
                  result
                }));
            }
            return result;
        }
    }
}

// 原始类
@L('log')
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);
复制代码

使用 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);
复制代码
相关文章
相关标签/搜索