TypeScript 初体验 - 环境搭建与编译执行
学习目标
- 学会搭建
TypeScript
环境 - 掌握
TypeScript
代码的编译与运行
环境搭建
TypeScript
编写的程序并不能直接通过浏览器运行,我们需要先通过 TypeScript
编译器把 TypeScript
代码编译成 JavaScript
代码
TypeScript
的编译器是基于 Node.js
的,所以我们需要先安装 Node.js
安装 Node.js
安装完成以后,可以通过 终端
或者 cmd
等命令行工具来调用 node
# 查看当前 node 版本
node -v
安装 TypeScript
编译器
通过 NPM
包管理工具安装 TypeScript
编译器
npm i -g typescript
安装完成以后,我们可以通过命令 tsc
来调用编译器
# 查看当前 tsc 编译器版本
tsc -v
编写代码
代码编辑器 - vscode
vsCode
和 TypeScript
都是微软的产品,vsCode
本身就是基于 TypeScript
进行开发的,vsCode
对 TypeScript
有着天然友好的支持
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)
基础类型
基础类型包含:string,number,boolean
标注语法
let title: string = '吧';
let n: number = 100;
let isOk: boolean = true;
空和未定义类型
因为在 Null
和 Undefined
这两种类型有且只有一个值,在标注一个变量为 Null
和 Undefined
类型,那就表示该变量不能修改了
let a: null;
// ok
a = null;
// error
a = 1;
默认情况下 null
和 undefined
是所有类型的子类型。 就是说你可以把 null
和 undefined
其它类型的变量
let a: number;
// ok
a = null;
如果一个变量声明了,但是未赋值,那么该变量的值为 undefined
,但是如果它同时也没有标注类型的话,默认类型为 any
,any
类型后面有详细说明
// 类型为 `number`,值为 `undefined`
let a: number;
// 类型为 `any`,值为 `undefined`
小技巧
因为 null
和 undefined
都是其它类型的子类型,所以默认情况下会有一些隐藏的问题
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
中的 String
、Number
、Boolean
,我们知道 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
}
在
strictNullChecks
为false
的情况下,undefined
和null
都可以赋值给void
,但是当strictNullChecks
为true
的情况下,只有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 仅能赋值给 unknow、any
- 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
载入核心的类型库
target
为es5
时:["dom", "es5", "scripthost"]
target
为es6
时:["dom", "es6", "dom.iterable", "scripthost"]
如果代码中使用了这些默认载入库以外的代码,则可以通过
lib
选项来进行设置
字面量类型
有的时候,我们希望标注的不是某个类型,而是一个固定值,就可以使用字面量类型,配合联合类型会更有用
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;
}
数字索引是字符串索引的子类型
注意:索引签名参数类型必须为 string 或 number 之一,但两者可同时出现
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');
}
面向对象编程
学习目标
- 掌握面向对象编程中类的基本定义与语法
- 学会使用类修饰符与寄存器
- 理解并掌握类的实例成员与类的静态成员的区别与使用
- 理解类与接口的关系,并熟练使用它们
- 了解类(构造函数)类型与对象类型的区别
类
面向对象编程中一个重要的核心就是:类
,当我们使用面向对象的方式进行编程的时候,通常会首先去分析具体要实现的功能,把特性相似的抽象成一个一个的类,然后通过这些类实例化出来的具体对象来完成具体业务需求。
类的基础
在类的基础中,包含下面几个核心的知识点,也是 TypeScript
与 EMCAScript2015+
在类方面共有的一些特性
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
了一个接口,那么就必须实现该接口中定义的契约 - 多个接口使用
,
分隔 implements
与extends
可同时存在
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
模块化的日趋成熟,TypeScript
对 ESM
模块系统的支持也是越来越完善
模块
无论是 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
中也称为外部模块。
模块语法
TypeScript
与 ESM
语法类似
导出模块内部数据
使用 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';
但是在 CommonJS
、AMD
中是没有默认值设置的,它们导出的是一个对象(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'
模块解析策略
为了兼容不同的模块系统(CommonJS
、ESM
),TypeScript
支持两种不同的模块解析策略:Node
、Classic
,当 --module
选项为:AMD
、System
、ES2015
的时候,默认为 Classic
,其它情况为 Node
--moduleResolution 选项
除了根据 --module
选项自动选择默认模块系统类型,我们还可以通过 --moduleResolution
选项来手动指定解析策略
// tsconfig.json
{
...,
"moduleResolution": "node"
}
Classic 模块解析策略
该策略是 TypeScript
以前的默认解析策略,它已经被新的 Node
策略所取代,现在使用该策略主要是为了向后兼容
相对导入
// /src/m1/a.ts
import b from './b.ts'
解析查找流程:
- src/m1/b.ts
默认后缀补全
// /src/m1/a.ts
import b from './b'
解析查找流程:
-
/src/m1/b.ts
-
/src/m1/b.d.ts
非相对导入
// /src/m1/a.ts
import b from 'b'
对于非相对模块的导入,则会从包含导入文件的目录开始依次向上级目录遍历查找,直到根目录为止
-
/src/m1/b.ts
-
/src/m1/b.d.ts
-
/src/b.ts
-
/src/b.d.ts
-
/b.ts
-
/b.d.ts
Node 模块解析策略
该解析策略是参照了 Node.js
的模块解析机制
相对导入
// node.js
// /src/m1/a.js
import b from './b'
在 Classic
中,模块只会按照单个的文件进行查找,但是在 Node.js
中,会首先按照单个文件进行查找,如果不存在,则会按照目录进行查找
- /src/m1/b.js
- /src/m1/b/package.json中'main'中指定的文件
- /src/m1/b/index.js
非相对导入
// node.js
// /src/m1/a.js
import b from 'b'
对于非相对导入模块,解析是很特殊的,Node.js
会这一个特殊文件夹 node_modules
里查找,并且在查找过程中从当前目录的 node_modules
目录下逐级向上级文件夹进行查找
- /src/m1/node_modules/b.js
- /src/m1/node_modules/b/package.json中'main'中指定的文件
- /src/m1/node_modules/b/index.js
- /src/node_modules/b.js
- /src/node_modules/b/package.json中'main'中指定的文件
- /src/node_modules/b/index.js
- /node_modules/b.js
- /node_modules/b/package.json中'main'中指定的文件
- /node_modules/b/index.js
TypeScript 模块解析策略
TypeScript
现在使用了与 Node.js
类似的模块解析策略,但是 TypeScript
增加了其它几个源文件扩展名的查找(.ts
、.tsx
、.d.ts
),同时 TypeScript
在 package.json
里使用字段 types
来表示 main
的意义
命名空间
命名空间
在 TS
中,export
和 import
称为 外部模块,TS
中还支持一种内部模块 namespace
,它的主要作用只是单纯的在文件内部(模块内容)隔离作用域
namespace k1 {
let a = 10;
export var obj = {
a
}
}
namespace k2 {
let a = 20;
console.log(k1.obj);
}
装饰器
学习目标
- 了解装饰器语法,学会使用装饰器对类进行扩展
- 清楚装饰器执行顺序
- 了解元数据以及针对装饰器的元数据编程
什么是装饰器
装饰器-Decorators
在 TypeScript
中是一种可以在不修改类代码的基础上通过添加标注的方式来对类型进行扩展的一种方式
- 减少代码量
- 提高代码扩展性、可读性和维护性
在
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
首先,需要安装 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);