最新TypeScript超详细笔记(全)

文章目录

一、初体验

1.0 学习目标

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

1.1 官网

https://www.typescriptlang.org/zh/

1.2 环境搭建

  • TypeScript 编写的程序并不能直接通过浏览器运行,需要先通过 TypeScript 编译器把TypeScript 代码编译成 JavaScript 代码
  • TypeScript的编译器是基于Node.js的,所以需要先安装Node.js

安装Node.js

  • https://nodejs.org
  • 安装完成以后,可以通过 终端 或者 cmd 等命令行工具来调用 node
# 查看当前 node 版本
node -v

安装TypeScript编译器

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

npm i -g typescript

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

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

1.3 编写代码

  • vsCodeTypeScript 都是微软的产品, vsCode 本身就是基于 TypeScript 进行开发的, vsCodeTypeScript 有着天然友好的支持
  • 默认情况下, TypeScript 的文件的后缀为 .ts
// ./src/helloKaiKeBa.ts
let str: string = '开课吧';

1.4 编译执行

  • 使用安装的 TypeScript 编译器 tsc.ts 文件进行编译
  • 默认情况下会在当前文件所在目录下生成同名的 js 文件
  • 还有几个常用的编译选项:outDir、target、watch,见下方
#此时文件是在src的上一级
tsc ./src/helloKaiKeBa.ts

–outDir

指定编译文件输出目录

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

–target

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

  • ES3中,let会被转成var
  • ES6中,let会被转成let
  • ES3中,对象最后一个属性后面的逗号是不允许存在的,ES5以后就可以
tsc --outDir ./dist --target ES6 ./src/helloKaiKeBa.ts

–watch

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

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

1.5 编译配置文件

  • 如果每次编译都输入这么一大堆的选项其实是很繁琐的
  • 好在 TypeScript 编译为提供了一个更加强大且方便的方式,编译配置文件tsConfig.json
  • 可以把编译的一些选项保存在一个指定的 json 文件中,默认情况下 tsc 命令运行的时候会自动去加载运行命令所在的目录下的 tsconfig.json 文件,配置文件格式如下
{
    "compilerOptions": {
        "outDir": "./dist",
        "target": "es5",
        "watch": true,
        // 检测null和undefined的标注
        "strictNullChecks": true,
        // 函数参数出现隐含的any时会报错
        "noImplicitAny": true,
        "lib": ["ES6", "DOM"],
        // 导入导出用commonjs还是ESM,见9.6 模块编译
        "module": "es6",
        // 允许加载JS文件
        "allowJs": true,
        // 导入导出策略
        "allowSyntheticDefaultImports": true,
        "esModuleInterop": true,
        //允许加载json
        "resolveJsonModule": true,
        // 解析策略
        "moduleResolution": "node",
        // 装饰器是否可用
        "experimentalDecorators": true,
        // 开启后会给类、方法、访问符、属性、参数等添加几个元数据
        "emitDecoratorMetadata": true
    },
    // ** : 所有目录(包括子目录)
 	// * : 所有文件,也可以指定类型 *.ts
    "include": ["./src/**/*"]
}
  • 有了单独的配置文件以后,可以直接运行
tsc

扩展库

  • TypeScript 在编译过程中只会转换语法,比如扩展运算符,箭头函数等
  • 对于API 是不会进行转换的(也没必要转换,而是引入一些扩展库进行处理的)
  • 如果代码中使用了 target 中没有的 API ,则需要手动进行引入
  • 默认情况下 TypeScript 会根据target 载入核心的类型库
    • targetes5 时: ["dom", "es5", "scripthost"]
    • targetes6 时: ["dom", "es6", "dom.iterable", "scripthost"]
    • 如果代码中使用了这些默认载入库以外的代码,则可以通过 lib 选项来进行设置

指定加载配置文件

  • 使用 --project-p 指定配置文件目录,会默认加载该目录下的 tsconfig.json 文件
  • 此时tsconfig.json这个文件是存放在configs文件夹下的
# configs文件夹下只有一个文件
tsc -p ./configs

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

tsc -p ./configs/ts.json

1.6 ts-node

可以直接运行ts文件,不需要经过编译

安装

npm i -g ts-node

使用

ts-node ./src/1-装饰器.ts

二、类型系统介绍

2.0 学习目标

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

2.1 什么是类型

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

数据是有格式的

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

程序是可能有错误的

  • 计算错误(对非数字类型数据进行一些数学运算)
  • 调用一个不存在的方法
  • 不同类型的数据有不同的操作方式或方法,如:字符串类型的数据就不应该直接参与数学运算

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

  • 动态类型语言
    • 程序运行期间才做数据类型检查的语言,如:JavaScript
  • 静态类型语言
    • 程序编译期间做数据类型检查的语言,如:Java

静态类型语言的优缺点

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

动态类型语言

  • 优点
    • 静态类型语言的缺点
  • 缺点
    • 静态类型语言的优点

2.2 什么是类型系统

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

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

类型标注

  • 类型标注就是在代码中给数据(变量、函数(参数、返回值))添加类型说明
  • 当一个变量或者函数(参数)等被标注以后就不能存储或传入与标注类型不符合的类型
  • 有了标注, TypeScript 编译器就能按照标注对这些数据进行类型合法检测
  • 有了标注,各种编辑器、IDE等就能进行智能提示

类型检测

  • 顾名思义,就是对数据的类型进行检测。注意这里,重点是类型两字。
  • 类型系统检测的是类型,不是具体值(虽然,某些时候也可以检测值)
    • 比如某个参数的取值范围(1-100之间)
    • 我们不能依靠类型系统来完成这个检测,它应该是我们的业务层具体逻辑
    • 类型系统检测的是它的值类型是否为数字

2.3 类型标注

  • TypeScript 中,类型标注的基本语法格式为
    • 数据载体:类型
  • TypeScript 的类型标注,我们可以分为
    • 基础的简单的类型标注
    • 高级的深入的类型标注

2.4 基础的简单的类型标注

  • 基础类型标注
    • 字符串、数字、布尔值、空、未定义
  • 非基础类型标注
    • 对象、数组
  • 特殊类型
    • 元组、枚举、无值类型、Never类型、任意类型、未知类型
  • 函数基本标注

基础类型

基础类型包含: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 赋值给其它类型的变量

也就是说声明了其他类型的时候可以赋值为null或者undefined`

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 ,避免很多常见问题,也可以使程序编写更加严谨

  • tsconfig.json中添加配置
  • 严格定义检查
  • 设置以后,就不可以将null类型赋值给其他类型
let a:number;
a = null;
// error
a.toFixed(1);

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

对象类型

内置对象类型

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

注意,这个字母开头是大写

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 关键字,在后面的接口章节中会详细讲解
// 里面用的分号
interface Person {
 username: string;
 age: number;
};
let a: Person = {
 username: 'zMouse',
 age: 35
};
// ok
a.username;
a.age;
// error
a.gender;

类与构造函数
  • 优点:功能相对强大,定义实体的同时也定义了对应的类型
  • 缺点:复杂,比如只想约束某个函数接收的参数结构,没有必要去定一个类,使用接口会更加简单
  • 定义的解构中包含具体功能用class,只是定义个解构用interface
// 类的具体使用,也会在后面的章节中讲解
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 中也是一样
    • string是字符串
    • new String是字符串对象
    • 字符串对象有的,字符串不一定有
    • 字符串对象赋值给字符串的时候会损失数据
    • 而字符串赋值给字符串对象的时候则不会
let a: string;
a = '1';
// error String有的,string不一定有(对象有的,基础类型不一定有)
a = new String('1');
let b: String;
b = new String('2');
// ok 和上面正好相反
b = '2';

数组类型

TypeScript中的数组是:一类具有相同特性的数据的有序结合

  • 相同类型的数据
  • 有序

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

枚举类型

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

注意事项:

  • key 不能是数字
  • value
    • 可以是数字,称为 数字类型枚举
    • 也可以是字符串,称为 字符串类型枚举
    • 但不能是其它值,默认为数字:0
数字类型枚举
  • 枚举值可以省略,如果省略,则:
    • 第一个枚举值默认为:0
    • 非第一个枚举值为上一个数字枚举值 + 1
  • 枚举值为只读(常量)
    • 所以变量名为全大写的
    • 初始化后不可修改
  • 看下方的自枚举
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;

// 上述编译后的结果
// HTTP_CODE['OK']=200
// HTTP_CODE[200]='OK'
// 自枚举
var HTTP_CODE;
(function (HTTP_CODE) {
    HTTP_CODE[HTTP_CODE["OK"] = 200] = "OK";
    HTTP_CODE[HTTP_CODE["NOT_FOUND"] = 404] = "NOT_FOUND";
    HTTP_CODE[HTTP_CODE["METHOD_NOT_ALLOWED"] = 405] = "METHOD_NOT_ALLOWED";
})(HTTP_CODE || (HTTP_CODE = {}));

字符串类型枚举
  • 枚举类型的值,也可以是字符串类型
  • 注意:如果前一个枚举值类型为字符串,则后续枚举项必须手动赋值
  • 枚举名称可以是大写,也可以是小写,推荐使用全大写(通常使用全大写的命名方式来标注值为常量)
enum URLS {
 USER_REGISETER = '/user/register',
 USER_LOGIN = '/user/login',
 // 如果前一个枚举值类型为字符串,则后续枚举项必须手动赋值
 INDEX = 0
}

无值类型

  • 表示没有任何数据的类型,通常用于标注无返回值函数的返回值类型,函数默认标注类型为: void
  • strictNullChecksfalse 的情况下, undefinednull 都可以赋值给 void
  • strictNullCheckstrue 的情况下,只有 undefined 才可以赋值给 void
function fn():void {
// 没有 return 或者 return undefined
}

Never类型

  • 当一个函数永远不可能执行 return 的时候,返回的就是 never
  • void 不同, void 是执行了return , 只是没有值
  • never 是不会执行 return ,比如抛出错误,导致函数终止执行
function fn(): never {
throw new Error('error');
}

任意类型

  • 有的时候,我们并不确定这个值到底是什么类型或者不需要对该值进行类型检测,就可以标注为 any类型
  • 一个变量申明未赋值且未标注类型的情况下,默认为 any 类型
  • 任何类型值都可以赋值给 any 类型
  • any 类型也可以赋值给任意类型
  • any 类型有任意属性和方法
  • 注意:标注为 any 类型,也意味着放弃对该值的类型检测,同时放弃 IDE 的智能提示
  • 小技巧:当指定 noImplicitAny 配置为 true ,当函数参数出现隐含的 any 类型时报错
// 隐式的any
let a;
let a1:any;
a1='a'
a1='2'

let b:number;
b=a;

未知类型

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

  • unknow 仅能赋值给 unknowany
  • unknow 没有任何属性和方法
let c:unknown='开课吧';
let b:number=1;
b.toFixed(1);
// 下面会报错
b=c;

函数类型

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

  • 函数名称( 参数1: 类型, 参数2: 类型… ): 返回值类型;
  • 函数更多的细节内容,在后期有专门的章节来进行深入的探讨
function add(x: number, y: number): number {
return x + y;
}

三、接口

3.0 学习目标

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

3.1 接口定义

  • TypeScript的核心之一就是对值(数据)所具有的结构就行类型检查
  • 接口(interface):对复杂的对象类型进行标注的一种方式,或者给其它代码定义一种契约(比如:类)
  • 接口中多个属性之间可以使用 逗号 或者 分号 进行分隔
  • 注意:接口是一种 类型 ,不能作为 值 使用,见下面错误的代码
// 定义接口
// 多个属性之间可以用逗号或者分号进行分隔
interface Point {
  x: number;
  y: number;
}
let p1: Point = {
  x: 100,
  y: 100
};

let p2 = Point; //错误

3.2 可选属性

  • 接口也可以定义可选的属性,通过 ? 来进行标注
  • 通过?标注的属性有两个类型可选:自身定义的,以及undefined
// color? 表示该属性是可选的
interface Point {
  x: number;
  y: number;
  color?: string;
}

3.3 只读属性

  • 可以通过 readonly 来标注属性为只读
  • 标注了一个属性为只读,那么该属性除了初始化以外,是不能被再次赋值的
interface Point {
  x: number;
  readonly y: number;
}
let p1: Point = {
  x: 100,
  y: 100
};
p1.y=200	//报错

3.4 任意属性

  • 希望给一个接口添加任意属性,可以通过索引类型来实现
  • [prop: string] : number
    • 中括号内,定义索引签名参数的类型
      • prop就是一个标识,可以随便起名
    • 后面是值的类型
  • 这个任意属性就是开个接口,可以自定义属性名和值的范围

数字类型索引

interface Point {
  x: number;
  y: number;
  color?: string;
  // 下面的索引类型也满足了上面color
  // 会报错:类型string|undefined属性的color,不能赋值给字符串索引类型number
 [prop: string]: number;
}

// 解决办法1
color?:string;
[prop:number]:number

// 解决办法2
color? :number;
[prop:string]:number | undefined;

字符串类型索引

  • 数字索引是字符串索引的子类型
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; // 正确
}

class Person {
  constructor(public username: string) {}
}
class Student extends Person {}
interface Point {
  [key: string]: Person;
  [key: number]: Student;
}

3.5 使用接口描述函数

  • 使用接口来描述一个函数
  • 使用接口来单独描述一个函数,是没 key 的
  • 作用
    • 为了复用
    • 回调函数
interface IFunc {
	// fn(a:string):string;		// 这种写法是错误的,他定义出来的是IFunc的一个方法
	(a: string): string;
}
let fn: IFunc = function(a) {}
let fn1: IFunc = function(a){}

interface IFunc{
  (x:number,y:number):number
}
let fn1: IFunc = function (a, b) {
  return a + b;
};
function todo(callback:IFunc){
  let v=callback(1,2)
}
todo(function(a:number,b:number):number{
  return a+b;
})

interface IEventFunc{
  (e:Event):void
  // (e:MouseEvent):void
}

function on(el:HTMLElement,evname:string,callback:IEventFunc){}
let div=document.querySelector('div');
if(div){
  on(div,'click',function(e){
    e.target
  })
}

3.6 接口合并

  • 多个同名的接口合并成一个接口
  • 如果合并的接口存在同名的非函数成员,则必须保证他们类型一致,否则编译报错
  • 接口中的同名函数则是采用重载(具体后期函数详解中讲解)
interface Box {
  height: number;
  width: number;
}
interface Box {
  scale: number;
}
let box: Box = {height: 5, width: 6, scale: 10}

四、高级类型

4.0 学习目标

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

4.1 联合类型

  • 联合类型也可以称为多选类型,当我们希望标注一个变量为多个类型之一时可以选择联合类型标注,的关系
  • 类似js中的或 |
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]);  // 错误
}

4.2 交叉类型

  • 交叉类型也可以称为合并类型,可以把多种类型合并到一起成为一种新的类型,并且 的关系
  • 类似js中的且 &
// 对一个对象进行扩展
interface o1 {x: number, y: string};
interface o2 {z: number};
let o: o1 & o2 = Object.assign({}, {x:1,y:'2'}, {z: 100});
// 下面的也可以
let o:o1&o2={
  x:100,
  y:'a',
  z:300
}

4.3 字面量类型

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

function setPosition(
  ele: Element,
  direction: "left" | "top" | "right" | "bottom"
) {}
let box = document.querySelector("div");
// ok
box && setPosition(box, 'bottom');
// error
box && setPosition(box, 'hehe');

4.4 类型别名

  • 有的时候类型标注比较复杂,这个时候我们可以类型标注起一个相对简单的名字
  • type 关键字
// 下面这个字面量类型是可以与基础类型等混合在一起的,但是不建议这么做,比如说设置一个'center'
type dir1 = 'left' | 'top' | 'right' | 'bottom' | string;
type dir = 'left' | 'top' | 'right' | 'bottom';
function setPosition(ele: Element, direction: dir) {
// ...
}

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

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

type callback = (a: string) => string;
let fn: callback = function(a) {return 'a'};
// 或者直接
let fn: (a: string) => string = function(a) {}

interface与type的区别

interface
  • 只能描述 object / class / function 的类型
  • 同名 interface 自动合并,利于扩展
type
  • 不能重名
  • 能描述所有数据

4.5 类型推导

  • 每次都显式标注类型会比较麻烦,TypeScript 提供了一种更加方便的特性:类型推导
  • TypeScript 编译器会根据当前上下文自动的推导出对应的类型标注,这个过程发生在
    • 初始化变量
    • 设置函数默认参数值
    • 返回函数值
// 自动推断 x 为 number
let x = 1;
// 不能将类型“"a"”分配给类型“number”
x = 'a';
// 函数参数类型、函数返回值会根据对应的默认值和返回值进行自动推断
function fn(a = 1) {return a * a}

4.6 类型断言

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

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

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

  • img 的类型为 Element
  • Element 类型其实只是元素类型的通用类型
  • 如果去访问 src 这个属性是有问题的
  • 要把它的类型标注得更为精确:HTMLImageElement 类型
  • 这个时候,就可以使用类型断言,它类似于一种 类型转换
let img = <HTMLImageElement>document.querySelector('#img');
// 或者
let img = document.querySelector('#img') as HTMLImageElement;

let img = document.querySelector("#img");
if (img) {
    // img.src	// 会报错,因为src不是Element里的通用属性
    // 下面两种方法都可以
    (<HTMLImageElement>img).src
    (img as HTMLImageElement).src
}

五、函数详解

5.0 学习目标

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

5.1函数的标注

  • 一个函数的标注包含:
    • 参数
    • 返回值
function fn(a: string): string {return ''};

interface ICallBack {
    (a: string): string;
}
let fn: ICallBack = function(a) {return ''};

// 只有这一个是箭头函数
let fn: (a: string) => string = function(a) {return ''};

// 这里也是箭头函数
type callback = (a: string) => string;
let fn: callback = function(a) {return ''};

5.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<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 );
}

// 上面的比较复杂了,直接assign即可
function merge(target:IObj,...others:Array<IObj>){
    return Object.assign(target,...others);
}
let newObj = merge({x: 1}, {y: 2}, {z: 3});

5.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);
        // 下面的也会报错
        (<t>this).b
    }
}
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;
    fnn:(x:number)=>void;
}
let obj2: T = {
    a: 2,
    fn(this: T) {
        return () => {
            // T
            console.log(this);
        }
    },
    fnn(this:Window,x:number){
        return ()=>{
            // Window
            this
        }
    }
}

5.4 函数重载

有的时候,同一个函数会接收不同类型的参数返回不同类型的返回值,可以使用函数重载来实现

  • 下面的例子,创造了多个同名函数,但是函数后面没有{}
  • 最后跟了一个可选范围比较大(any)的同名函数
  • 这样在匹配的时候可以具体匹配什么
  • noImplicitAny不能设置为true,会报错
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;
}
// attr:PlainObject  ,这个attr就变成了个对象
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[key] = attr[key];
        }
    }
}
let div = document.querySelector('div');
if (div) {
    css(div, 'width', '100px');
    css(div, {width: '100px'});
    // error,如果不使用重载,这里就会有问题了
    css(div, 'width');
}

六、面向对象编程

6.0 学习目标

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

6.1 类

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

类的基础

在类的基础中,包含下面几个核心的知识点,也是 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 {
    // 这种方法过于繁琐,见下方public
    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 就是类的默认修饰符,表示该成员可以在任何地方进行读写操作
    • 添加了public修饰符以后
      • 给当前类添加同名的成员属性
        • constructor外面的idusername就不用进行标注了
      • 类实例化的时候,会把传入的参数值赋值给对应的成员属性
        • constructor内的this.id=id也可以省略
  • protectedprivatereadonly也会让其变为类的属性,也就是可以通过this来调用
    • 但是上面的都有作用域范围
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');

6.2 继承

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

class VIP extends User {}

super关键字

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

  • 如果子类没有重写构造函数,则会在默认的 constructor 中调用 super()
  • 如果子类有自己的构造函数,则需要在子类构造函数中显示的调用父类构造函数 : super(//参数) ,否则会报错
  • 在子类构造函数中只有在 super(//参数) 之后才能访问 this,父类的属性
    • 写在super之前就不行了
  • 在子类中,可以通过 super 来访问父类的成员属性和方法
    • 父类的属性用this即可
    • 父类的方法是否被重写
      • 重写了:super.父类的方法
      • 没有重写:this.父类的方法
  • 通过 super 访问父类的的同时,会自动绑定上下文对象为当前子类 this
class VIP extends User {
    constructor(
        id: number,
        username: string,
        public score = 0
    ) {
        super(id, username);
        console.log(this.id);
    }
    postAttachment(file: string): void {
        console.log(`${this.username} 上传了一个附件: ${file}`)
    }
}
let vip1 = new VIP(1, 'Leo');
vip1.postArticle('标题', '内容');
vip1.postAttachment('1.png');

方法的重写与重载

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

  • 重写
    • 重新实现了父类的方法:参数的个数和参数的类型一致
  • 重载
    • 参数个数、参数类型不同
  • 子类中重写了父类的方法,还要用父类的方法
    • super.父类的方法()
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关键字调用父类方法
        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');

6.3 修饰符

  • 有的时候,我们希望对类成员(属性、方法)进行一定的访问控制,来保证数据的安全

  • 通过 类修饰符 可以做到这一点,目前 TypeScript 提供了四种修饰符:

    • public:公有,默认
    • protected:受保护
      • 可以访问,但是不能外部修改
    • private:私有
      • 外部包括子类不能访问也不可修改
      • 自身可以修改
    • readonly:只读
      • 可以访问,但是一旦确定不能修改
  • 倒不是说就不能通过外面的数据修改内部的属性,比如说protected、private等

    • 可以通过set,比如
    class User{
        constructor(
        	private _password:string
        ){}
        set password(password:string){
            this._password=password
        }
    }
    let p1=new User('abc')
    console.log(p1);		// {_password:'abc'}
    p1.password='bdc'
    console.log(p1);		// {_password:'bcd'}
    
    

public修饰符

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

  • 自身
  • 子类
  • 类外

修改级别为:

  • 自身
  • 子类
  • 类外

protected修饰符

它的访问级别为:

  • 自身
  • 子类

修改级别:

  • 自身
  • 子类

private修饰符

它的访问级别为:

  • 自身

修改级别:

  • 自身

readonly修饰符

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

  • 自身
  • 子类
  • 类外

修改级别:

class User {
    constructor(
    // 可以访问,但是一旦确定不能修改
    readonly id: number,
     // 可以访问,但是不能外部修改
     protected username: string,
     // 外部包括子类不能访问,也不可修改
     private password: string
    ) {
        // ...
    }
    // ...
    protected method(){}
}
let user1 = new User(1, 'zMouse', '123456');

6.4 寄存器

  • 有的时候,我们需要对类成员 属性 进行更加细腻的控制,就可以使用 寄存器 来完成这个需求
  • 通过寄存器 ,可以对类成员属性的访问进行拦截并加以控制,更好的控制成员属性的设置和访问边界
  • 寄存器分为两种:
    • 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 '******';
    }
    // ...
}

6.5 静态成员

  • 成员属性和方法都是实例对象的
  • 有的时候需要给类本身添加成员
  • 区分某成员是静态还是实例的:
    • 该成员属性或方法是类型的特征还是实例化对象的特征
    • 如果一个成员方法中没有使用或依赖 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)来进行访问,而是直接通过类名访问(不管是类内还是类外)
  • 静态成员也可以通过访问修饰符进行修饰
  • 静态成员属性一般约定(非规定)全大写

6.6 抽象类

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

现在前端比较流行组件化设计,比如 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的时候,调用render实际走的是父类的render方法,并不是预期的效果),更应该从代码层面上去约束子类必须得有 render 方法,否则编码就不能通过

abstract关键字

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

abstract class Component<T1, T2> {
    public state: T2;
    constructor(
    public props: T1
    ) {
    }
    public abstract render(): string;
}

  • 使用抽象类有一个好处:
    • 约定了所有继承子类的所必须实现的方法,使类的设计更加的规范
  • 使用注意事项:
    • abstract 修饰的方法不能有方法体,也就是没有{}
    • 如果一个类有抽象方法,那么该类也必须为抽象的
    • 如果一个类是抽象的,那么就不能使用 new 进行实例化(因为抽象类表名该类有未实现的方法,所以不允许实例化)
      • 不能new
    • 如果一个子类继承了一个抽象类,那么该子类就必须实现抽象类中的所有抽象方法,否则该类还得声明为抽象的
      • 抽象类中的render()方法是抽象的,子类如果没有定义render()方法会报错

6.7 类与接口

  • 通过接口,可以为对象定义一种结构和契约
  • 还可以把接口与类进行结合,通过接口,让类去强制符合某种契约
  • 从某个方面来说,当一个抽象类中只有抽象的时候,它就与接口没有太大区别了
  • 这个时候,我们更推荐通过接口的方式来定义契约
    • 抽象类编译后还是会产生实体代码,而接口不会
    • 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;
}
// 继承以后IStorage既有getInfo()又有save()
interface IStorage extends ILog {
  save(data: string): void;
}

6.8 类与对象类型

当在 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: '男'|'女'): Person;
    type: string;
}

在使用的时候要格外注意

  • 主要是传入的是实例,还是可以new的
function fn1(arg: Person /*如果希望这里传入的Person 的实例对象*/) {
    arg.eat();
}
fn1( new Person('', 1, '男') );		// 这里的new是上面的class类

// 下面的typeof Person返回的是class类的构造函数
function fn2(arg: typeof Person /*如果希望传入的Person构造函数*/) {
    new arg('', 1, '男');
}
// 上面的fn2与下面的fn3相同,写法不同
function fn3(arg:PersonConstructor){
    new arg('', 1, '男');
}
fn2(Person);

七、类型系统深入

7.0 学习目标

  • 理解什么是类型保护,并能使⽤类型保护去优化代码逻辑
  • 熟悉类型操作符的使⽤,进⼀步理解类型系统

7.1 类型保护

  • 通常在 JavaScript 中通过判断来处理⼀些逻辑
  • TypeScript 中这种条件语句块还有另外⼀个特性:根据判断逻辑的结果,缩⼩类型范围(有点类似断⾔)
  • 这种特性称为 类型保护 ,触发条件:
    • 逻辑条件语句块:if、else、elseif
    • 特定的⼀些关键字:typeof、instanceof、in……

typeof

typeof 可以返回某个数据的类型,在 TypeScriptifelse 代码块中能够把typeof 识别为类型保护,推断出适合的类型

function fn(a: string|number) {
    // error,不能保证 a 就是字符串
    a.substring(1);
    if (typeof a === 'string') {
        // ok
        a.substring(1);
    } else {
        // ok
        a.toFixed(1);
    }
}

instanceof

typeof 类似的, instanceof 也可以被 TypeScript 识别为类型保护

function fn(a: Date|Array<any>|RegExp) {
    if (a instanceof Array) {
        a.push(1);
    } else if(a instanceof Date){
        a.getFullYear();
    }else if(a instanceof RegExp){
        
    }
}

in

in 也是如此

interface IA {
    x: string;
    y: string;
}
interface IB {
    a: string;
    b: string;
}
function fn(arg: IA | IB) {
    if ('x' in arg) {
        // ok
        arg.x;
        // error
        arg.a;
    } else {
        // ok
        arg.a;
        // error
        arg.x;
    }
}

字面量类型保护

如果类型为字⾯量类型,那么还可以通过该字⾯量类型的字⾯值进⾏推断

interface IA {
    type: 'IA';
    x: string;
    y: string;
}
interface IB {
    type: 'IB';
    a: string;
    b: string;
}
function fn(arg: IA | IB) {
    if (arg.type === 'IA') {
        // ok
        arg.x;
        // error
        arg.a;
    } else {
        // ok
        arg.a;
        // error
        arg.x;
    }
}

自定义类型保护

有的时候,以上的⼀些⽅式并不能满⾜⼀些特殊情况,则可以⾃定义类型保护规则

data is Element[]|NodeList 是⼀种类型谓词,格式为: xx is XX ,返回这种类型的函数就可以被 TypeScript 识别为类型保护

function canEach(data: any): data is Element[]|NodeList {
    return data.forEach !== undefined;
}
function fn2(elements: Element[]|NodeList|Element) {
    if ( canEach(elements) ) {
        elements.forEach((el: Element)=>{
            el.classList.add('box');
        });
    } else {
        elements.classList.add('box');
    }
}

7.2 类型操作

TypeScript 提供了⼀些⽅式来操作类型这种数据,但是需要注意的是,类型数据只能作为类型来使⽤,⽽不能作为程序中的数据,这是两种不同的数据,⼀个⽤在编译检测阶段,⼀个⽤于程序执⾏阶段

typeof

TypeScript 中, typeof 有两种作⽤

  • 获取数据的类型
  • 捕获数据的类型
let str1 = 'kaikeba';
// 如果是 let ,把 'string' 作为值
let t = typeof str1;
// 如果是 type,把 'string' 作为类型
type myType = typeof str1;
let str2: myType = '开课吧';
let str3: typeof str1 = '开课吧';

let obj={
    name:'aa',
    age:30
}
// 用type,a的值是{name:string,age:number}
type a = typeof obj;
// 用let,a的值是Object
let a = typeof obj;

keyof

获取类型的所有 key 的集合

集合的形式为 ‘aa’ | ‘bb’ | ‘cc’ | …

interface Person {
  name: string;
  age: number;
  location: string;
}

type K1 = keyof Person; // "name" | "age" | "location"
type K2 = keyof Person[];  // number | "length" | "push" | "concat" | ...
type K3 = keyof { [x: string]: Person };  // string | number


class Person {
  name: string = "Semlinker";
}

let sname: keyof Person;
sname = "name";

let K1: keyof boolean; // let K1: "valueOf"
let K2: keyof number; // let K2: "toString" | "toFixed" | "toExponential" | ...
let K3: keyof symbol; // let K1: "valueOf"

type P1 = Person["name"];  // string
type P2 = Person["name" | "age"];  // string | number
type P3 = string["charAt"];  // (pos: number) => string
type P4 = string[]["push"];  // (...items: string[]) => number
type P5 = string[][0];  // string


interface Person {
    name: string;
    age: number;
};
type personKeys = keyof Person;
// 等同:type personKeys = "name" | "age"
// 
let p1 = {
    name: 'zMouse',
    age: 35
}
function getPersonVal(k: personKeys) {
    return p1[k];
}
type a = typeof p1;
// 上面a的值是{name:string,age:number}
function getPersonVal(k:keyof typeof p1){
    return p1[k]
}
/**
等同:
function getPersonVal(k: 'name'|'age') {
return p1[k];
}
*/
getPersonVal('name'); //正确
getPersonVal('gender'); //错误

// 另外一个范例
type Todo = {
  id: number;
  text: string;
  done: boolean;
}

const todo: Todo = {
  id: 1,
  text: "Learn TypeScript keyof",
  done: false
}

function prop<T extends object, K extends keyof T>(obj: T, key: K) {
  return obj[key];
}

const id = prop(todo, "id"); // const id: number
const text = prop(todo, "text"); // const text: string
const done = prop(todo, "done"); // const done: boolean

in

针对类型进⾏操作的话,内部使⽤的 for…in 对类型进⾏遍历

下面例子的目的是把age的number类型转化成string类型

也就是说遍历原本的类型,都转化成统一类型

interface Person {
    name: string;
    age: number;
}
type personKeys = keyof Person;
type newPerson = {
    [k in personKeys]: number;
    /**
等同 [k in 'name'|'age']: number;
也可以写成
[k in keyof Person]: number;
*/
}
/**
type newPerson = {
name: number;
age: number;
}
*/

注意: in 后⾯的类型值必须是 string 或者 number 或者 symbol

7.3 类型兼容

TypeScript 的类型系统是基于结构⼦类型的,它与名义类型(如:java)不同(名义类型的数据类型兼容性或等价性是通过明确的声明或类型的名称来决定的)。这种基于结构⼦类型的类型系统是基于组成结构的,只要具有相同类型的成员,则两种类型即为兼容的。

class Person {
    name: string;
    age: number;
}
class Cat {
    name: string;
    age: number;
}
function fn(p: Person) {
    p.name;
}
let xiaohua = new Cat();
// ok,因为 Cat 类型的结构与 Person 类型的结构相似,所以它们是兼容的
fn(xiaohua);

interface IFly{
  fly():void
}
class Person implements IFly{
  name:string;
  age:number;
  study(){}
  fly(){}
}
class Cat{
  name:string;
  age:number;
  catchMouse(){}
}
// 没有定义IFly的时候,这里放Person或者Cat,如果有对应的属性或者方法还好,如果没有就出问题了
function fn2(arg:IFly){
  arg.fly();
}
fn2(pp)
// error 没有fly方法
fn2(cc)

八、泛型

8.0 学习目标

  • 理解泛型概念与使用场景
  • 在函数、类、接口中使用泛型

8.1 为什么要使用泛型

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

function getVal(obj, k) {
    return obj[k];
}

  • 上面的函数,我们想实现的是获取一个对象指定的 k 所对应的值
  • 那么实际使用的时候,obj 的类型是不确定的,自然 k 的取值范围也是不确定的
  • 它需要我们在具体调用的时候才能确定,这个时候这种定义过程不确定类型的需求就可以通过泛型来解决

8.2 泛型的使用-函数

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

function getVal<T>(obj: T, k: keyof T) {
    return obj[k];
}
let obj1={
  x:1,
  y:2
}
let obj2={
  username:'zmouse',
  age:35
}
// 这里的typeof obj1实际上等于type obj=typeof obj1  getVal<obj>
getVal<typeof obj1>(obj1,'x')
getVal<typeof obj2>(obj2,'age')

8.3 泛型类

在面向对象章节中,我们曾经给大家讲过一个基于泛型使用的例子:模拟组件

abstract class Component<T1, T2> {
    props: T1;
    state: T2;
    constructor(props: T1) {
        this.props = props;
    }
    abstract render(): string;
}
interface IMyComponentProps {
    val: number;
}
interface IMyComponentState {
    x: number;
}
class MyComponent extends Component<IMyComponentProps, IMyComponentState> {
    constructor(props: IMyComponentProps) {
        super(props);
        this.state = {
            x: 1
        }
    }
    render() {
        this.props.val;
        this.state.x;
        return '<myComponent />';
    }
}
let myComponent = new MyComponent({val: 1});
myComponent.render();

8.4 泛型接口

还可以在接口中使用泛型

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

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

根据接口,我们封装对应的一些方法

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

但是,我们会发现该接口的 data 项的具体格式不确定,不同的接口会返回的数据是不一样的,当我们想根据具体当前请求的接口返回具体 data 格式的时候,比较麻烦了,因为 getData 并不清楚你调用的具体接口是什么,对应的数据又会是什么样的
这个时候我们可以对 IResponseData 使用泛型

interface IResponseData<T> {
    code: number;
    message?: string;
    data: T;
}
function getData<U>(url: string) {
    return fetch(url).then(res => {
        return res.json();
    }).then( (data: IResponseData<U>) => {
        return data;
    });
}

定义不同的数据接口

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

调用具体代码

(async function(){
    let user = await getData<IResponseUserData>('');
    if (user.code === 1) {
        console.log(user.message);
    } else {
        console.log(user.data.username);
    }
    let articles = await getData<IResponseArticleData>('');
    if (articles.code === 1) {
        console.log(articles.message);
    } else {
        console.log(articles.data.id);
        console.log(articles.data.author.username);
    }
})();

九、模块系统

9.0 学习目标

  • 了解模块化的演进过程
  • 不同的模块化标准的使用与它们之间的差异性
  • 熟悉 TypeScript 模块化的使用以及与其它模块化系统的差异
  • 熟悉 TypeScript 模块化与其它模块化系统之间的编译与转换
  • 学会使用 TypeScript 模块化进行项目开发

9.1 模块化

模块化是指自上而下把一个复杂问题(功能)划分成若干模块的过程,在编程中就是指通过某种规则对程序(代码)进行分割、组织、打包,每个模块完成一个特定的子功能,再把所有的模块按照某种规则进行组装,合并成一个整体,最终完成整个系统的所有功能

从基于 Node.js 的服务端 commonjs 模块化,到前端基于浏览器的 AMDCMD 模块化,再到 ECMAScript2015 开始原生内置的模块化, JavaScript 的模块化方案和系统日趋成熟。

TypeScript 也是支持模块化的,而且它的出现要比 ECMAScript模块系统标准化要早,所以在 TypeScript 中即有对 ECMAScript 模块系统的支持,也包含有一些自己的特点

9.2 模块化历程

  • CommonJS
  • AMD
  • UMD
  • ESM

无论是那种模块化规范,重点关注:保证模块独立性的同时又能很好的与其它模块进行交互

  • 如何定义一个模块与模块内部私有作用域
  • 通过何种方式导出模块内部数据
  • 通过何种方式导入其它外部模块数据

9.3 基于服务端、桌面端的模块化

CommonJS

在早期,对于运行在浏览器端的 JavaScript 代码,模块化的需求并不那么的强烈,反而是偏向 服务端、桌面端 的应用对模块化有迫切的需求(相对来说,服务端、桌面端程序的代码和需求要复杂一些)。CommonJS 规范就是一套偏向服务端的模块化规范,它为非浏览器端的模块化实现制定了一些的方案和标准,NodeJS 就采用了这个规范。

独立模块作用域

一个文件就是模块,拥有独立的作用域

导出模块内部数据

通过 module.exportsexports 对象导出模块内部数据

  • 原理
    • 每个模块(文件中)都有一个exports对象,这个对象默认是个空对象
      • 每个模块都是一个module的实例对象
      • 所以commonjs中实际导出的是module对象
      • 但是commonjs内部默认做了module.exports = exports
        • 这个赋值的操作是在模块最顶层,后续改exports不会影响到module.exports的值
      • 等于说module.exports和exports指向同一个对象
      • 后面module.exports = {} 就把指向变了
    • require这个函数,想办法拿到参数那个模块的exportsmodule对象
      • 导入的时候哪怕导入的是个对象,整个js文件也从上往下运行了一遍
      • 加载过程是同步的,也就是先导入,再执行自身的内容,这就是为什么要放在最上面
      • 模块被多次引入时,会缓存,最终只加载(运行)一次
        • 哪怕main.js里 加载bar和foo,foo里加载bar,实际bar只加载了一次
        • commonjs中的module有一个loaded属性,加载过以后就变成了true
    • 两个模块之间的exports依然保留着引用关系
    • 所以commonjs的本质就是对象的引用赋值
    • 加载过程是同步的,也就是先导入,再执行自身的内容,这就是为什么要放在最上面
    • 加载的时候采用的是深度优先搜索
// a.js
let a = 1;
let b = 2;

setTimeout(()=>{
    a=20
}.1000)

setTimeout(()=>{
    console.log(b);		// aaa
},2000)

// or 实际上是把exports这个对象导出去了,那么b.js在require的时候导入的a就是这个exports对象
exports.x = a;
exports.y = b;

// 注意,这个定时器是基于module.exports = exports ,下面module.exports就改变指向了
setTimeout( () => {
    module.exports.name = 'bb'
    console.log(exports.name);		// 'bb'
},3000)

// module.exports = exports 变成了module.exports = {}
module.exports = {
  x: a,
  y: b
}


导入外部模块数据

通过 require 函数导入外部模块数据

查找顺序:

  • 情况一:X是一个核心模块,比如path、http
    • 直接返回核心模块,并且停止查找
  • 情况二:X是以 ./ 或 …/ 或 /(根目录)开头的
    • 第一步:将X当做一个文件在对应的目录下查找
      • 如果有后缀名,按照后缀名的格式查找对应的文件
      • 如果没有后缀名,会按照如下顺序
        • 直接查找文件X
        • 查找X.js文件
        • 查找X.json文件
        • 查找X.node文件
    • 第二步:没有找到对应的文件,将X作为一个目录
      • 查找目录下面的index文件
        • 查找X/index.js文件
        • 查找X/index.json文件
        • 查找X/index.node文件
  • 情况三:直接是一个X(没有路径),并且X不是一个核心模块
    • 详细的见下方node里面的非相对导入,会逐层往上找node_modules中的
  • 如果上面的路径中都没有找到,那么报错:not found
// b.js
let a = require('./a');
a.x;
a.y;
setTimeout(()=>{
    console.log(a.x);		// 20
},2000)

setTimeout(()=>{
    a.y='aaa'
},1000)

9.4 基于浏览器的模块化

AMD

因为 CommonJS 规范一些特性(基于文件系统,同步加载),它并不适用于浏览器端,所以另外定义了适用于浏览器端的规范

异步模块定义:

AMD(Asynchronous Module Definition)

https://github.com/amdjs/amdjs-api/wiki/AMD

浏览器并没有具体实现该规范的代码,我们可以通过一些第三方库来解决

requireJS

https://requirejs.org/

// 1.html
// data-main加载的是程序的入口文件,也就是第一个JS文件
<script data-main="js/a" src="https://cdn.bootcss.com/require.js/2.3.6/require.min.js"></script>

独立模块作用域

通过一个 define 方法来定义一个模块,在该方法内部模拟模块独立作用域

// b.js
define(function() {
  // 模块内部代码
})

导出模块内部数据

通过 return 导出模块内部数据

// b.js
define(function() {
    let a = 1;
    let b = 2;
    return {
        x: a,
        y: b
    }
})

导入外部模块数据

通过前置依赖列表导入外部模块数据

// a.js
// 定义一个模块,并导入 ./b 模块
define(['./b'], function(m2) {
	console.log(m2.x);	// m2就是上面的对象{x:a,y:b}
})

requireJSCommonJS 风格

require.js 也支持 CommonJS 风格的语法

CMD

  • 跟amd的区别是导入的时机和导出的方式

导出模块内部数据

// b.js
define(function(require, exports, module) {
    let a = 1;
    let b = 2;
    // module.exports===exports
    // 但是用exports导出的话得是  exports.x=a   exports.y=b
    module.exports = {
        x: a,
        y: b
    }
})

导入外部模块数据

// a.js
define(function(require, exports, module) {
    let b = require('./b')
    console.log(b);
})

UMD

严格来说,UMD 并不属于一套模块规范,它主要用来处理 CommonJSAMDCMD 的差异兼容,是模块代码能在前面不同的模块环境下都能正常运行。随着 Node.js 的流行,前端和后端都可以基于 JavaScript 来进行开发,这个时候或多或少的会出现前后端使用相同代码的可能,特别是一些不依赖宿主环境(浏览器、服务器)的偏低层的代码。我们能实现一套代码多端适用(同构),其中在不同的模块化标准下使用也是需要解决的问题,UMD 就是一种解决方式

(function (root, factory) {
    if (typeof module === "object" && typeof module.exports === "object") {
        // Node, CommonJS-like
        module.exports = factory();
    }
    else if (typeof define === "function" && define.amd) {
        // AMD 模块环境下
        define(factory);
    } else {
        // 不使用任何模块系统,直接挂载到全局
        root.kkb = factory();
    }
}(this, function () {
    let a = 1;
    let b = 2;

    // 模块导出数据
    return {
        x: a,
        y: b
    }
}));

// 另外一种写法,帮助理解
// 可以跟上面相互补充
M(function(){
    funciton fn1(){
        console.log('fn1');
    }
    return {fn1}
})
(function M(root, factory) {
    if (typeof module === "object" && typeof module.exports === "object") {
        // Node, CommonJS-like
        module.exports = factory();
    }
    else if (typeof define === "function" && define.amd) {
        // AMD 模块环境下
        define(factory);
    }else{
        root.f=factory();
    }
}(this.function(){}))

9.5 模块化的大同世界

ESM

ECMAScript2015/ECMAScript6 开始,JavaScript 原生引入了模块概念,而且现在主流浏览器也都有了很好的支持,同时在 Node.js 也有了支持,所以未来基于 JavaScript 的程序无论是在前端浏览器还是在后端 Node.js 中,都会逐渐的被统一

独立模块作用域

一个文件就是模块,拥有独立的作用域,且导出的模块都自动处于 严格模式 下,即:'use strict'

script 标签需要声明 type="module"

打开的html页面必须用live server

import和export是关键字,不是对象,也不是函数

<script>标签引用的时候要加type=“module”,并且要用live server打开

导出模块内部数据

使用 export 语句导出模块内部数据

// 导出单个特性
export let name1, name2,, nameN;
export let name1 =, name2 =,, nameN;
export function FunctionName(){...}
export class ClassName {...}

// 导出列表 这个大括号,不是对象,里面不能用键值对 export {a:1}这种形式
// {防止要导出的变量的引用列表}
export { name1, name2,, nameN };

// 重命名导出
export { variable1 as name1, variable2 as name2,, nameN };

// 默认导出
export default expression;
export default function () {}
export default function name1() {}
export { name1 as default,};

// 模块重定向导出
export * from;
export { name1, name2,, nameN } from;
export { import1 as name1, import2 as name2,, nameN } from;
export { default } from;

导入外部模块数据

导入分为两种模式

  • 静态导入
  • 动态导入

静态导入

使用 import 语句导入模块,这种方式称为:静态导入

静态导入方式不支持延迟加载,import 必须在模块的最开始

import defaultExport from "module-name";
import * as name from "module-name";
import { export } from "module-name";
import { export as alias } from "module-name";
import { export1 , export2 } from "module-name";
import { foo , bar } from "module-name/path/to/specific/un-exported/file";
import { export1 , export2 as alias2 , [...] } from "module-name";
import defaultExport, { export [ , [...] ] } from "module-name";
import defaultExport, * as name from "module-name";
import "module-name";

// 下面的示例是错误的
document.onclick = function () {

    // import 必须放置在当前模块最开始加载
    // import m from './m.js'

    // console.log(m);

}

动态导入

此外,还有一个类似函数的动态 import(),它不需要依赖 type="module" 的 script 标签。

关键字 import 可以像调用函数一样来动态的导入模块。以这种方式调用,将返回一个 promise

document.onclick = function () {
  import("./m.js").then((data) => {
    // 这个data是个module对象,数据存放在这个对象的属性下面
    console.log(data);
    console.log(data.x);
  });
};

// 也支持 await
let m = await import('./m.js');

通过 import() 方法导入返回的数据会被包装在一个对象中,即使是 default 也是如此

9.6 TypeScript 中的模块化

TypeScript 也支持模块化,而且它的出现比 ESM 还要早,TypeScript 的模块化实现也有一些地方与上述其它一些模块化系统有所差异,但是随着 TypeScript 的更新,同时 ESM 标准本身也越来越成熟,所以当下和未来 TypeScript 的模块化也会与 ESM 越来越接近

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
  • 可以看下target="es3"或者target="es5"或者target="es6"编译后的效果
{
  "compilerOptions": {
    "outDir": "./dist",
    "target": "es5",
    "watch": true,
    "strictNullChecks": true,
    "noImplicitAny": true,
    "lib": ["ES6", "DOM"],
    "module": "es6"
  },
  "include": ["./src/**/*"]
}

模块导出默认值的问题

如果一个模块没有默认导出

// 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 '开课吧';

但是在 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);

9.7 命名空间

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

namespace k1 {
    let a = 10;
    export var obj = {
        a
    }
}
namespace k1 {
  let b = 20;
  export var obj2 = {
    b
  };
}
namespace k2 {
    let a = 20;
    console.log(k1.obj);
}

var k1;
(function (k1) {
    var a = 10;
    k1.obj = {
        a: a
    };
})(k1 || (k1 = {}));
(function (k1) {
    var b = 20;
    k1.obj2 = {
        b: b
    };
})(k1 || (k1 = {}));
var k2;
(function (k2) {
    var a = 20;
    console.log(k1.obj);
})(k2 || (k2 = {}));

9.8 模块解析策略

什么是模块解析

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

相对与非相对模块导入

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

相对导入

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

// 导入根目录下的 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’中指定的文件

    {
      "name":"d",
      "version": "1.0.0",
      "main":"d.ts"
    }
    
    
  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

    • 按照上面后缀查找完了以后没有找到,就认为是个文件夹,比如说(b)
    • 然后找文件b下的index.js
    • 还找不到就去找package.jsonmain对应的文件
  • TypeScriptpackage.json 里使用字段 types 来表示 main 的意义

    • package.json 中main指定的文件是js的时候是没有标注的,比如g.js
    • 此时要创建一个.d.ts文件,比如g.d.ts
    • 然后在type里指定这个文件,用来做标注
    {
      "name":"g",
      "version": "1.0.0",
      "main":"./dist/g.js",
      "types":"./g.d.ts"
    }
    // 具体见ts-demo-7中的import g from 'g'
    
    

十、 装饰器

10.0 学习目标

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

10.1 什么是装饰器

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

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

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

10.2 装饰器语法

装饰器的使用极其的简单

  • 装饰器本质就是一个函数
  • 通过特定语法在特定的位置调用装饰器函数即可对数据(类、方法、甚至参数等)进行扩展
  • 装饰器的执行并不是在类执行的时候才调用,而是在类被创建的过程中就调用
    • 按照范例的下面不执行M.add或者M.sub,装饰器会执行
  • 装饰器的三个参数(装饰的是函数的时候):
    • target:被装饰的方法所属的类,也就是对应的构造函数,所以是Function
    • name:被装饰的方法的名称
    • descriptor:描述符
      • 类似Object.defineProperty中的第三个参数,描述enumerable、writable、configurable、value的
      • 其中的value的值就是被装饰的函数,也就是上面的name

启用装饰器特性

  • 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);

// 可以通过ts-node来执行以下看看结果

10.3 装饰器

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

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() {}
}

function d1(target: Function) {
  console.log(target);
}
function d11(target: Function) {
  console.log(target,1111111);
}
function d2(target: any, name: string) {
  console.log(typeof target, name);
}

function d3(target: any, name: string, descriptor: PropertyDescriptor) {
  console.log(typeof target, name, descriptor);
}
function d4(target: any, name: string, descriptor: PropertyDescriptor) {
  console.log(typeof target, name, descriptor);
}
function d5(target: any, name: string, index: number) {
  // name当前参数所在的方法
  console.log(typeof target, name, index);
}

@d1
@d11
class MyClass {
  @d2
  a: number;

  @d2
  static property1: number;

  @d3
  get b() {
    return 1;
  }

  @d3
  static get c() {
    return 2;
  }

  @d4
  public method1(@d5 x: number, @d5 y: number) {}

  @d4
  public static method2() {}
}

类装饰器

目标

  • 应用于类的构造函数

参数

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

方法装饰器

目标

  • 应用于类的方法上

参数

  • 第一个参数
    • 静态方法:类的构造函数
    • 实例方法:类的原型对象—new出来的对象
  • 第二个参数
    • 方法名称
  • 第三个参数
    • 方法描述符对象
      • descriptor.value,就是该方法本身
      • descriptor.value.length是参数的个数

属性装饰器

目标

  • 应用于类的属性上

参数

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

顺序

  • 先执行实例属性
  • 后执行静态属性
  • 跟先后顺序没有关系

访问器装饰器

目标

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

参数

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

参数装饰器

目标

  • 应用在参数上

参数

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

10.4 装饰器执行顺序

实例装饰器

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

静态装饰器

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

​ 类

function d1(target: Function) {
  console.log('d1');
  console.log(target);
}
function d11(target: Function) {
  console.log('d11');
  console.log(target, 1111111);
}
function d2(target: any, name: string) {
  console.log('d2');
  console.log(typeof target, name);
}

function d3(target: any, name: string, descriptor: PropertyDescriptor) {
  console.log('d3');
  console.log(typeof target, name, descriptor);
}
function d4(target: any, name: string, descriptor: PropertyDescriptor) {
  console.log('d4');
  console.log(typeof target, name, descriptor);
}
function d5(target: any, name: string, index: number) {
  // name当前参数所在的方法
  console.log('d5');
  console.log(typeof target, name, index);
}

@d1
@d11
class MyClass {
  @d2
  a: number;

  @d2
  static property1: number;

  @d3
  get b() {
    return 1;
  }

  @d3
  static get c() {
    return 2;
  }

  @d4
  public method1(@d5 x: number, @d5 y: number) {}

  @d4
  public static method2() {}
}
/*
d2
d3
d5
d5
d4
d2
d3
d4
d11
d1
*/

10.5 装饰器工厂

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

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

function L(type: string) {
  // 类装饰器的时候target指向的是类的构造函数
  return function (target: Function) {
    target.prototype.type = type;
  };
}
function log(type?: string) {
  return function (
    target: any, // 标注any是为了在对象上挂数据
    name: string,
    descriptor: PropertyDescriptor
  ) {
    // log方法装饰器 是比 L类装饰器先执行的
    // 所以在这里拿prototype.type的时候是undefined
    let value = descriptor.value;
    descriptor.value = function (x: number, y: number) {
      let result = value(x, y);
      let _type = type;
      if (!_type) {
        // target==='function'是静态方法时target是构造函数
        _type = typeof target === "function" ? target.prototype.type : target.type;
      }
      console.log({
        type: _type,
        name,
        x,
        y,
        result
      });
      return result;
    };
  };
}
@L("log")
class M {
  @log("storage")
  static add(x: number, y: number) {
    return x + y;
  }
  @log()
  static sub(x: number, y: number) {
    return x - y;
  }
}
let v1 = M.add(1, 2);
console.log(v1);
let v2 = M.sub(1, 2);
console.log(v2);
export default {};

10.6 元数据

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

什么是元数据?

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

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

10.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 方法调用来添加 元数据
    • propertyKey在数据是属性或者方法时,就是属性名或者方法名
  • 通过 @Reflect.metadata 装饰器来添加 元数据
    • @Reflect.metadata对类进行装饰的时候还是调用的 Reflect.defineMetadata
    • 只不过不用写target 以及propertyKey两个参数了
import "reflect-metadata"

@Reflect.metadata("n", 5)
class A {
    @Reflect.metadata("n", 6)
    public static method1() {
    }
  
  	@Reflect.metadata("n", 7)
  	public method2() {
    }
    
    @Reflect.metadata('h', 10)
  	public abc: number = 10;
}

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

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

console.log(Reflect.getMetadata('n', A));					// 1
console.log(Reflect.getMetadata('n', A, 'method1'));		// 2
console.log(Reflect.getMetadata('n', obj));					// 3
console.log(Reflect.getMetadata('n', obj, 'method2'));		// 7
console.log(Reflect.getMetadata('k', obj, 'method2'));		// 4
console.log(Reflect.getMetadata('h', obj, 'abc'));			// 10

获取

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

// 同样的例子,直接用Reflect.metadata装饰类
import 'reflect-metadata';
// function L(type: string) {
//   return function (target: Function) {
//     // target.prototype.type = type;
//     Reflect.defineMetadata('type', type, target);
//   };
// }
function log(type?: string) {
  return function (
    target: any, // 标注any是为了在对象上挂数据
    name: string,
    descriptor: PropertyDescriptor
  ) {
    // log方法装饰器 是比 L类装饰器先执行的
    // 所以在这里拿prototype.type的时候是undefined
    let value = descriptor.value;
    descriptor.value = function (x: number, y: number) {
      let result = value(x, y);
      let _type = type;
      if (!_type) {
        // target==='function'是静态方法时target是构造函数
        // _type = typeof target === "function" ? target.prototype.type : target.type;
        if (typeof target === 'function') {
          _type = Reflect.getMetadata('type', target);
        } else {
          _type = Reflect.getMetadata('type', target.constructor);
        }
      }
      console.log({
        type: _type,
        name,
        x,
        y,
        result
      });
      return result;
    };
  };
}
// @L('log')
@Reflect.metadata('type', 'log')
class M {
  @log('storage')
  static add(x: number, y: number) {
    return x + y;
  }
  @log()
  static sub(x: number, y: number) {
    return x - y;
  }
}
let v1 = M.add(1, 2);
console.log(v1);
let v2 = M.sub(1, 2);
console.log(v2);
export default {};


10.8 使用 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);

// 另外一个范例
import 'reflect-metadata';
function f() {
  return function (target: any, name: string, descriptor: PropertyDescriptor) {
    console.log(descriptor.value.length);
    console.log(Reflect.getMetadata('design:type', target, name));
    console.log(Reflect.getMetadata('design:paramtypes', target, name));
    console.log(Reflect.getMetadata('design:returntype', target, name));

    let _t = Reflect.getMetadata('design:paramtypes', target, name)[0];

    let value = descriptor.value;
    if (_t === Number) {
      value(100);
    }
    if (_t === String) {
      value('开课吧');
    }
    if (_t === Date) {
      value(new Date());
    }
  };
}
class B {
  name: string;
  constructor() {}
  @f()
  method1(a: string, b: number): string {
    console.log();
    return 'a';
  }

  @f()
  // 改变下面的Date为Number、string
  method2(x?: Date) {
    console.log(x);
  }
}

let b = new B();
b.method2();

  • 3
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 12
    评论
评论 12
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值