TypeScript


为什么学习 TS

  • 获得更好的开发体验
  • 解决 JS 中难以解决的问题
function getUserName() {
  if (Math.random() < 0.5) {
    return "袁进";
  }
  return 404;
}
let myname = getUserName(); //名字可能错了,类型也可能错误
myname = myname
  .split(" ")
  .filter((it) => it) //过滤空的
  .map((it) => it[0].toUpperCase() + it.substr(1))
  .join(" ");

JS 的问题

  • 使用了不存在的变量,函数或成员
  • 把一个不确定的类型当做一个确定的类型处理
  • 在使用 undefined null 的成员

JS 原罪

  • JS 语言本身特性决定了改语言无法适应大型复杂项目
  • 弱类型:某个变量,可以随时更换类型
  • 解释性:错误的发生时间,是在运行时

前端开发中,大部分时间都在排错

TypeScript

TS 是 JS 的超集,是一个可选的,静态的类型系统

类型系统:对代码中所有的标识符(变量,函数,参数,返回值)进行类型检查

静态的:无论浏览器环境,还是 node,无法直接识别 TS 代码

babel:es6—>es5
tsc:ts—->es
tsc:ts 编译器

静态:类型检查发生的时间,在编译的时候,而非运行时

TS 不参与任何运行时候的类型检查,运行的是 JS 代码

TS 常识

额外的惊喜

有了类型检查,增强了面向对象的开发

JS 中, 也有类和对象,JS 支持面向对象开发。没有类型检查,很多面向对象的场景实现起来有诸多问题

使用 TS 后,可以编写出完善的面向对象代码。

在 node 中搭建 TS 开发环境

安装:cnpm i -g typescript

默认情况 TS 会做出假设:

  • 假设当前执行环境是 DOM
  • 如果使用代码中没有使用模块化语句,便认为改代码全局执行
  • 编译的目标代码是 ES3

有两种方式更改以上假设:

  • 使用 tsc 命令时加上选项参数
  • 使用 TS 配置文件,更改编译选项

TS 配置文件

  1. tsconfig.json
  2. tsc –init 生成配置文件

使用配置文件后,使用 tsc 进行编译时,不能跟上文件名,如果跟上的话,会忽略配置文件

直接 tsc 即可编译

{
  "compilerOptions": {//编译选项
    "target":"es2016",//配置编译目标代码的版本标准
    "module":"commonjs",//配置编译目标使用的模块化标准
    "lib":["es2016"],//这里没有node环境可以配置,但是去掉了浏览器环境,
    // 也不知道这是node环境,所以导致console也没有了,这里必须要安装第三大库
    "outDir":"./dist/"//dist里面放编译结果
  },
  "include":["./src"]//要编译的文件夹。默认整个工程
}

第三方库:@types/node

@types 是一个 ts 官方的类型库 其中包含了很多对 JS 代码的类型描述。(第三方库 axios,lodash,mock 等是 JS 写的,没有类型检查,我需要类型检查,就去 types 里面找有没有对应的类型库)

jQuery:js 写的,没有类型检查
安装@types/jquery,为 jquery 库添加类型定义

cnpm i -D @types/node 开发依赖,运行时候不需要,所以-D

编译和运行流程:

得先 tsc 编译 ts
在 node index.js 运行 js
嫌麻烦,所以衍生出用第三方库简化流程

第三方库简化编译运行流程

ts-node:编译完没有 dist 目录,没有 js 文件,直接运行 js

cnpm i -g ts-node 安装后,运行用 ts-node src/index.ts,编译完直接执行,没有 dist 文件

监控代码变化,变化后再次编译运行

nodemon: 用于检测文件变化

cnpm i -g nodemon

执行命令:nodemon –exec ts-node .\src\index.ts

可以把这个命令弄到 package.json 里面

两个细节

  • nodemon 检测的范围太广,我只想让他检测 ts
"dev":"nodemon -e ts --exec ts-node ./src/index.ts"//表示检测文件的拓展名ts
  • src 文件夹之外的 ts 文件
"dev":"nodemon --watch src -e ts --exec ts-node ./src/index.ts"//只监控文件夹src的

最终可以使用 tsc 完成最后的打包

基本类型约束

TS 是一个可选的静态的类型系统

如何进行类型约束

仅需要在 变量、函数的参数、函数的返回值位置加上:类型

function sum(a: number, b: number): number {
  return a + b;
}
console.log(sum(3, 4));

ts 在很多场景中可以完成类型推导

any: 表示任意类型,对该类型,ts 不进行类型检查

小技巧,如何区分数字字符串和数字,关键看怎么读?
如果按照数字的方式朗读,则为数字;否则,为字符串。

源代码和编译结果的差异

编译结果中没有类型约束信息

基本类型

  • number:数字
  • string:字符串
  • boolean:布尔
  • 数组
  • object: 对象
  • null 和 undefined

boolean

function isOdd(n: number): boolean {
  return n % 2 === 0;
}
console.log(isOdd(3));

数组

let num: number[]; //num必须是数组,数组里每一项都是number
num = [1, 2];
// 或语法糖
let num: Array<number> = [3, 4, 5];

object: 对象

let u: object;
u = {
  name: "abc",
  age: 19,
  // 不能约束对象里面的东西
};

应用场景

// 传入一个对象,打印对象所有属性值
function printValues(obj: object) {
  const vals = Object.values(obj); //返回的是一个any类型的数组(不知道数组每一项是什么的数组)
  vals.forEach((v) => {
    console.log(v);
  });
}
printValues({
  name: "abc",
  age: 19,
});

null 和 undefined 是所有其他类型的子类型,它们可以赋值给其他类型

let m: string = null; //隐患产生了
let n: string = undefined; //隐患产生了
n.toLocaleLowerCase(); //报错

通过在 tsconfig.json 里面添加strictNullChecks:true,可以获得更严格的空类型检查,null 和 undefined 只能赋值给自身。

其他常用类型

  • 联合类型:多种类型任选其一

配合类型保护进行判断

let name: string | undefined; // 联合类型
// name.的时候没有提示了
if (typeof name === "string") {
  // name.  确定为string出现提示了
}
//即类型保护

类型保护:当对某个变量进行类型判断之后,在判断的语句块中便可以确定它的确切类型,typeof 可以触发基本类型保护。

  • void 类型:通常用于约束函数的返回值,表示该函数没有任何返回

  • never 类型:通常用于约束函数的返回值,表示该函数永远不可能结束

  • 字面量类型:使用一个值进行约束

  • 元祖类型(Tuple): 一个固定长度的数组,并且数组中每一项的类型确定

  • any 类型: any 类型可以绕过类型检查,因此,any 类型的数据可以赋值给任意类型

类型别名

对已知的一些类型定义名称

type 类型名 = ...
type Gender = "男" | "女";
type User = {
  name: string;
  age: number;
  gender: Gender;
};
let u: User;

u = {
  name: "ads",
  gender: "男",
  age: 34,
};

function getUsers(g: Gender): User[] {
  return [];
}
getUsers("女");

函数的相关约束

函数重载:在函数实现之前,对函数调用的多种情况进行声明

function combine(a: number, b: number): number;
function combine(a: string, b: string): string;
function combine(a: number | string, b: number | string): number | string {
  //两个数字,相乘返回;
  //两个字符串,拼接返回
  if (typeof a === "number" && typeof b === "number") {
    return a * b;
  } else if (typeof a === "string" && typeof b === "string") {
    return a + b;
  }
  throw new Error("a和b必须是相同类型");
}
const result1 = combine(1, 2);
const result2 = combine("1", "2");

可选参数:可以在某些参数名后加上问号,表示该参数可以不用传递。可选参数必须在参数列表的末尾。

默认参数

扩展类型-枚举

扩展类型:类型别名、枚举、接口、类

枚举通常用于约束某个变量的取值范围。

字面量和联合类型配合使用,也可以达到同样的目标。

字面量类型的问题

  • 在类型约束位置,会产生重复代码。可以使用类型别名解决该问题。
type Gender = "男" | "女";
let gender: Gender;
gender = "男";
function searchGender(g: Gender) {}
  • 逻辑含义和真实的值产生了混淆,会导致当修改真实值的时候,产生大量的修改。
type Gender = "帅哥" | "美女"; //把男女改为帅哥美女
  • 字面量类型不会进入到编译结果。

枚举

如何定义一个枚举:

enum 枚举名{
    枚举字段1 = 值1,
    枚举字段2 = 值2,
    ...
}

逻辑名称和真实名称区分开来

enum Gender{
    male = "男",
    female = "女"
}
let gender =  Gender.male;
console.log(gender)

如果需要改逻辑名称:F2

枚举会出现在编译结果中,编译结果中表现为对象。

enum Gender {
  male = "男",
  female = "女",
}
let gender = Gender.male;

function printGenders() {
  const vals = Object.values(Gender);
  vals.forEach((v) => console.log(v));
}
printGenders();

枚举的规则:

  • 枚举的字段值可以是字符串或数字
  • 数字枚举的值会自动自增
enum Level {
  level1 = 1, //不赋值就是0,后面依次递增
  level2,
  level3,
}
let l: Level = Level.level1;
l = Level.level2;
console.log(l); //2
  • 被数字枚举约束的变量,可以直接赋值为数字
  • 数字枚举的编译结果 和 字符串枚举有差异

最佳实践:

  • 尽量不要在一个枚举中既出现字符串字段,又出现数字字段
  • 使用枚举时,尽量使用枚举字段的名称,而不使用真实的值(逻辑含义与真实区分开)

扩展:位枚举(枚举的位运算)

针对的数字枚举

位运算:两个数字换算成 2 进制后进行的计算

enum Permission {
  // 通过2进制位上的标识确定权限
  Read = 1, //2^0   0001
  Write = 2, //2^1  0010
  Create = 4, //2^2 0100
  Delete = 8, //2^3 1000
}
//3 == 0011 可读,可写
// 1. 如何组合权限:    |:或运算
// let p1 = Permission.Read | Permission.Write;
let p2: Permission = Permission.Read | Permission.Write;

// 2. 如何判断是否拥有某个权限

function hasPermission(target: Permission, per: Permission) {
  //target里面包不包含per
  return (target & per) === per; //且运算
}
// 判断变量p2是否拥有可读权限
hasPermission(p2, Permission.Read);

// 3. 如何删除某个权限

p2 = p2 ^ Permission.Write; //异或(两个位置相同取0不同取1)

模块化

相关配置:

配置名称 含义
module 设置编译结果中使用的模块化标准
moduleResolution 设置解析模块的模式
noImplicitUseStrict 编译结果中不包含”use strict”
removeComments 编译结果移除注释
noEmitOnError 错误时不生成编译结果
esModuleInterop 启用 es 模块化交互非 es 模块导出

前端领域中的模块化标准:ES6、commonjs、amd、umd、system、esnext

TS 中如何书写模块化语句
编译结果??

TS 中如何书写模块化语句

TS 中,导入和导出模块,统一使用 ES6 的模块化标准

如果不写导入,可以快速修复

必须是声明导出方式,如果使用默认导出,就没有智能导入了

export default {
  name: "kevin",
  sum(a: number, b: number) {
    return a + b;
  },
};

导入的时候不要添加.ts,因为编译后是 js 了,但是编译结果没有 js 文件

编译结果中的模块化

可配置:在配置文件中设置

"module":"commonjs",

TS 中的模块化在编译结果中:

  • 如果编译结果的模块化标准是 ES6: 没有区别
  • 如果编译结果的模块化标准是 commonjs:导出的声明会变成 exports 的属性,默认的导出会变成 exports 的 default 属性;导入:把整个对象拿到,依次取属性

解决默认导入的错误

如何在 TS 中书写 commonjs 模块化代码(一般不会遇到此问题)

如果使用之前的 commonjs 的话,就失去类型检查功能了
可以这样导入(条件:必须经过 esModuleInterop 的配置):import myModule from ‘./myModule’,就有类型检查了

使用以下语法就可以解决:

导出:export = xxx

导入:import xxx = require(“xxx”)

模块解析

模块解析:应该从什么位置寻找模块

TS 中,有两种模块解析策略

  • classic:经典(过时)
  • node:node 解析策略(唯一的变化,是将 js 替换为 ts)
    • 相对路径require("./xxx") 先找当前路径有没有,没有的话看package.json的main.ts,没有就这个文件夹下的index.ts
    • 非相对模块require("xxx") 当前文件夹有没有node-modules,在node-modules下有没有改模块

为了防止出错,强行 node 解析策略:加上配置:”moduleResolution”:”node”

接口和类型兼容性

扩展类型-接口

接口:inteface

扩展类型:类型别名、枚举、接口、类

TypeScript 的接口:用于约束类、对象、函数的契约(标准)

契约(标准)的形式:

  • API 文档,弱标准
  • 代码约束,强标准

和类型别名一样,接口,不出现在编译结果中

  1. 接口约束对象
  2. 接口约束函数

对象

interface User {
  name: string;
  age: number;
}
// 和类型别名的区别:约束类
/* type User = {
    name:string,
    age:number
} */
let u: User = {
  name: "abc",
  age: 19,
};

函数

interface User {
  name: string;
  age: number;
  // 书写方式1    sayHello:() => void
  // 书写方式2    sayHello():void
  // 和类型别名一样,接口,不出现在编译结果中,所以不能写函数实现
  sayHello(): void;
}
let u: User = {
  name: "abc",
  age: 19,
  sayHello() {
    console.log("asda");
  },
};

类型别名约束函数

type Condition = (n: number) => boolean;

function sum(numbers: number[], callBack: Condition) {
  let s = 0;
  numbers.forEach((n) => {
    if (callBack(n)) {
      s += n;
    }
  });
  return s;
}
const result = sum([3, 4, 5, 6, 7], (n) => n % 2 !== 0);
console.log(result);

接口约束函数

interface Condition {
  (n: number): boolean;
}

// 可以这样写
/* type Condition = {//定界符
    (n:number):boolean
} */

function sum(numbers: number[], callBack: Condition) {
  let s = 0;
  numbers.forEach((n) => {
    if (callBack(n)) {
      s += n;
    }
  });
  return s;
}
const result = sum([3, 4, 5, 6, 7], (n) => n % 2 !== 0);
console.log(result);

接口可以继承

interface A {
  T1: string;
}
interface B extends A {
  //接口B里面有A里面所有成员
  T2: number;
}
let u: B = {
  T2: 33,
  T1: "ab",
};

可以通过接口之间的继承,实现多种接口的组合

interface A {
  T1: string;
}
interface B {
  T2: number;
}
interface C extends A, B {
  T3: boolean;
}
let u: C = {
  T2: 33,
  T1: "abc",
  T3: true,
};

使用类型别名可以实现类似的组合效果,需要通过&,它叫做交叉类型

type A = {
  T1: string;
};
type B = {
  T2: number;
};
type C = {
  T3: boolean;
} & A &
  B;

let u: C = {
  T2: 123,
  T1: "abc",
  T3: true,
};

它们的区别:

  • 子接口不能覆盖父接口的成员
  • 交叉类型会把相同成员的类型进行交叉 不是覆盖 无法正常赋值了

readonly

只读修饰符,修饰的目标是只读

只读修饰符不在编译结果中

类型兼容性

B->A,如果能完成赋值,则 B 和 A 类型兼容

鸭子辨型法(子结构辨型法):目标类型需要某一些特征,赋值的类型只要能满足该特征即可

  • 基本类型:完全匹配
  • 对象类型:鸭子辨型法

类型断言

interface Duck {
  sound: "嘎嘎嘎";
  swin(): void;
}
let person = {
  name: "伪装成鸭子的人",
  age: 11,
  //sound: "嘎嘎嘎",这里需要类型断言,因为这里的sound:嘎嘎嘎是string,但是要求sound是嘎嘎嘎字面量
  sound: "嘎嘎嘎" as "嘎嘎嘎",
  swin() {
    console.log(this.name + "正在游泳,并发出了" + this.sound + "的声音");
  },
};
let duck: Duck = person; //这个结构满足鸭子的特征,所以可以赋值了

解决的问题:假设有个函数,用于得到服务器某个接口的返回结果,是一个用户对象。对象有很多属性,但是实际需要的属性很少,就可以用鸭子辨型法。

当直接使用对象字面量赋值的时候,会进行更加严格的判断

  • 函数类型

一切无比自然

参数:传递给目标函数的参数可以少,但不可以多

interface Condition {
  (n: number, i: number): boolean;
}

function sum(numbers: number[], callBack: Condition) {
  let s = 0;
  for (let i = 0; i < numbers.length; i++) {
    const n = numbers[i];
    if (callBack(n, i)) {
      s += n;
    }
  }
  return s;
}
// const result = sum([3, 4, 5, 6, 7], n => n % 2 !== 0);
// const result = sum([3, 4, 5, 6, 7], (n, i) => i % 2 !== 0);
// console.log(result);

返回值:要求返回必须返回;不要求返回,你随意;

忠告:不要死记硬背,理解 TS,舒服安全逻辑地写代码

TS 中的类

面向对象思想

基础部分,学习类的时候,仅讨论新增的语法部分。

属性

使用属性列表来描述类中的属性

属性的初始化检查配置

strictPropertyInitialization:true

属性的初始化位置:

  1. 构造函数中
  2. 属性默认值

默认:性别男

方法 1:构造函数中

class User {
  name: string;
  age: number;
  gender: "男" | "女";
  constructor(name: string, age: number, gender: "男" | "女" = "男") {
    this.name = name;
    this.age = age;
    this.gender = gender;
  }
}
const u = new User("abc", 19);

方法 2:属性默认值

class User {
  name: string;
  age: number;
  gender: "男" | "女" = "男";
  constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
  }
}
const u = new User("abc", 1);
u.gender = "女"; //可改

属性可以修饰为可选的

属性可以修饰为只读的

class User {
  readonly id: number;
  name: string;
  age: number;
  gender: "男" | "女" = "男";
  pid?: string;
  constructor(name: string, age: number) {
    this.id = Math.random();
    this.name = name;
    this.age = age;
  }
}

不希望外部使用

class User {
  name: string;
  age: number;
  gender: "男" | "女" = "男";
  publishNumber: number = 3; //每天一个可以发布多少文章
  curNumber: number = 0; //当前可以发布的文章数
  constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
  }
}
const u = new User("abc", 1);
u.publishNumber; //不希望外部使用
u.curNumber; //不希望外部使用
// JS用Symble

使用访问修饰符

访问修饰符可以控制类中的某个成员的访问权限

  • public:默认的访问修饰符,公开的,所有的代码均可访问
  • private:私有的,只有在类中可以访问
  • protected:见进阶

属性简写

如果某个属性,通过构造函数的参数传递,并且不做任何处理的赋值给该属性。可以进行简写。
加上的是修饰符(public private …)

访问器

作用:用于控制属性的读取和赋值

方式 1:类似 java es6
方式 2:类似 c#

class User {
  constructor(public name: string, public _age: number) {}
  set age(value: number) {
    if (value < 0) {
      this._age = 0;
    } else if (value > 200) {
      this._age = 200;
    } else {
      this._age = value;
    }
  }
  get age() {
    return Math.floor(this._age);
  }
}

泛型

有时,书写某个函数时,会丢失一些类型信息(多个位置的类型应该保持一致或有关联的信息)

function take(arr: any[], n: number): any[] {
  if (n > arr.length) {
    return arr;
  }
  const newArr: any[] = [];
  for (let i = 0; i < n; i++) {
    newArr.push(arr[i]);
  }
  return newArr;
}
const newArr = take(["2", 3, 4, 5, 1], 3);
console.log(newArr);
// any应该相同

泛型:是指附属于函数、类、接口、类型别名之上的类型

泛型相当于是一个类型变量,在定义时,无法预先知道具体的类型,可以用该变量来代替,只有到调用时,才能确定它的类型

很多时候,TS 会智能的根据传递的参数,推导出泛型的具体类型

如果无法完成推导,并且又没有传递具体的类型,默认为空对象

泛型可以设置默认值(不能完成推导,又没有传递就使用默认值)

在函数中使用泛型

在函数名之后写上<泛型名称>

function take<T>(arr: T[], n: number): T[] {
  //T依附于函数
  if (n > arr.length) {
    return arr;
  }
  const newArr: T[] = [];
  for (let i = 0; i < n; i++) {
    newArr.push(arr[i]);
  }
  return newArr;
}
take<number>([1, 2], 2); //调用的时候才知道T的类型

如果不写泛型名称,会自动推导

如何在类型别名、接口、类中使用泛型

直接在名称后写上<泛型名称>

// 回调函数:判断数组中的某一项是否满足条件
// type callback = (n: number, i: number) => boolean;
type callback<T> = (n: T, i: number) => boolean;
// 封装一个filter
function filter<T>(arr: T[], callback: callback<T>): T[] {
  const newArr: T[] = [];
  arr.forEach((n, i) => {
    if (callback(n, i)) {
      newArr.push(n);
    }
  });
  return newArr;
}

const arr = [3, 4, 5, 2, 1, 6];
console.log(filter(arr, (n) => n % 2 !== 0));

// 接口
interface callback<T> {
  (n: T, i: number): boolean;
}

export class ArrayHelper<T> {
  constructor(private arr: T[]) {}
  take(n: number): T[] {
    if (n > this.arr.length) {
      return this.arr;
    }
    const newArr: T[] = [];
    for (let i = 0; i < n; i++) {
      newArr.push(this.arr[i]);
    }
    return newArr;
  }
  shuffle() {
    for (let i = 0; i < this.arr.length; i++) {
      const targetIndex = this.getRandom(0, this.arr.length);
      const temp = this.arr[i];
      this.arr[i] = this.arr[targetIndex];
      this.arr[targetIndex] = temp;
    }
  }
  private getRandom(min: number, max: number) {
    const dec = max - min;
    return Math.floor(Math.random() * dec + max);
  }
}
const helper = new ArrayHelper([1, 2]); //自动类型推导

helper.take(); //结果是number类型

泛型约束

泛型约束,用于现实泛型的取值

interface hasNameProperty {
  //nameToUpperCase函数只能传递满足此接口的类型
  name: string;
}
/**
 * 将某个对象name的属性的每个单词的首字母大写,并将该对象返回
 */
function nameToUpperCase<T extends hasNameProperty>(obj: T): T {
  obj.name = obj.name
    .split(" ")
    .map((s) => s[0].toUpperCase() + s.substr(1))
    .join(" ");
  return obj;
}
const o = {
  name: "kevin yuan",
  age: 19,
};
const newO = nameToUpperCase(o);
console.log(newO.name); //Kevin Yuan

多泛型

写函数或类,依赖多种类型

// 将两个数组混合[1,2] ['a','b'] === [1,'a',2,'b']
function mixinArray<T, K>(arr1: T[], arr2: K[]): (T | K)[] {
  if (arr1.length !== arr2.length) {
    throw new Error("长度不等");
  }
  let result: (T | K)[] = [];
  for (let i = 0; i < arr2.length; i++) {
    result.push(arr1[i]);
    result.push(arr2[i]);
  }
  return result;
}

const result = mixinArray([1, 2, 3], ["a", "b", "c"]);
result.forEach((it) => console.log(it));

深入理解类和接口

面向对象概述

为什么要讲面向对象

  1. TS 为前端面向对象开发带来了契机

JS 语言没有类型检查,如果使用面向对象的方式开发,会产生大量的接口,而大量的接口会导致调用复杂度剧增,这种复杂度必须通过严格的类型检查来避免错误,尽管可以使用注释或文档或记忆力,但是它们没有强约束力。

TS 带来了完整的类型系统,因此开发复杂程序时,无论接口数量有多少,都可以获得完整的类型检查,并且这种检查是据有强约束力的。

  1. 面向对象中有许多非常成熟的模式,能处理复杂问题

在过去的很多年中,在大型应用或复杂领域,面向对象已经积累了非常多的经验。

基于 ts 的 nextjs 相当于前端的 java spring

什么是面向对象

面向对象:Oriented(基于) Object(事物),简称 OO。

是一种编程思想,它提出一切以类对切入点思考问题。

其他编程思想:面向过程(模块化)、函数式编程

学开发最重要最难的是什么?思维

面向过程:以功能流程为思考切入点,不太适合大型应用

函数式编程:以数学运算为思考切入点

面向对象:以划分类为思考切入点。类是最小的功能单元

类:可以产生对象的模板。

如何学习

  1. TS 中的 OOP (面向对象编程,Oriented Object Programing)
  2. 小游戏练习

理解 -> 想法 -> 实践 -> 理解 -> ….

类的继承

继承的作用

继承可以描述类与类之间的关系

坦克、玩家坦克、敌方坦克
玩家坦克是坦克,敌方坦克是坦克

如果 A 和 B 都是类,并且可以描述为 A 是 B,则 A 和 B 形成继承关系:

  • B 是父类,A 是子类
  • B 派生 A,A 继承自 B
  • B 是 A 的基类,A 是 B 的派生类

如果 A 继承自 B,则 A 中自动拥有 B 中的所有成员

成员的重写

重写(override):子类中覆盖父类的成员

子类成员不能改变父类成员的类型

无论是属性还是方法,子类都可以对父类的相应成员进行重写,但是重写时,需要保证类型的匹配。

注意 this 关键字:在继承关系中,this 的指向是动态——调用方法时,根据具体的调用者确定 this 指向

super 关键字:在子类的方法中,可以使用super 关键字读取父类成员

类型匹配

鸭子辨型法
子类的对象,始终可以赋值给父类
let p:Tank = new EnemyTank()

面向对象中,这种现象,叫做里氏替换原则

如果需要判断一个数据的具体子类类型,可以使用 instanceof

protected 修饰符

readonly:只读修饰符

访问权限修饰符:private public protected

protected: 受保护的成员,只能在自身和子类中访问

单根性和传递性

单根性:每个类最多只能拥有一个父类

传递性:如果 A 是 B 的父类,并且 B 是 C 的父类,则,可以认为 A 也是 C 的父类

抽象类

为什么需要抽象类

有时,某个类只表示一个抽象概念,主要用于提取子类共有的成员,而不能直接创建它的对象。该类可以作为抽象类。

给类前面加上abstract,表示该类是一个抽象类,不可以创建一个抽象类的对象。

抽象成员

父类中,可能知道有些成员是必须存在的,但是不知道该成员的值或实现是什么,因此,需要有一种强约束,让继承该类的子类,必须要实现该成员。

抽象类中,可以有抽象成员,这些抽象成员必须在子类中实现。抽象成员必须出现在抽象类中。

abstract class Chess {
  x: number = 0;
  y: number = 0;
  abstract readonly name: string;
}
class Horse extends Chess {
  // 方式1:快速修复
  // readonly name:string = '马'
  // 方式2
  // readonly name: string
  // constructor() {
  //     super()
  //     this.name = '跑'
  // }
  // 方式3:访问器
  get name() {
    return "兵";
  } //没写set,本身就是只读的
}
abstract class Chess {
  x: number = 0;
  y: number = 0;
  abstract readonly name: string;
  abstract move(targetX: number, targetY: number): boolean;
}
class Horse extends Chess {
  move(targetX: number, targetY: number): boolean {
    this.x = targetX;
    this.y = targetY;
    console.log("移动成功");
    return true;
  }
  get name() {
    return "兵";
  }
}

设计模式 - 模板模式

设计模式:面对一些常见的功能场景,有一些固定的、经过多年实践的成熟方法,这些方法称之为设计模式。

模板模式:有些方法,所有的子类实现的流程完全一致,只是流程中的某个步骤的具体实现不一致,可以将该方法提取到父类,在父类中完成整个流程的实现,遇到实现不一致的方法时,将该方法做成抽象方法。

abstract class Chess {
  x: number = 0;
  y: number = 0;

  abstract readonly name: string;

  move(targetX: number, targetY: number): boolean {
    console.log("1. 边界判断");
    console.log("2. 目标位置是否有己方棋子");
    //3. 规则判断
    if (this.rule(targetX, targetY)) {
      this.x = targetX;
      this.y = targetY;
      console.log(`${this.name}移动成功`);
      return true;
    }
    return false;
  }

  protected abstract rule(targetX: number, targetY: number): boolean;
}

class Horse extends Chess {
  protected rule(targetX: number, targetY: number): boolean {
    return true;
  }

  readonly name: string = "马";
}

class Pao extends Chess {
  protected rule(targetX: number, targetY: number): boolean {
    return false;
  }

  readonly name: string;

  constructor() {
    super();
    this.name = "炮";
  }
}

class Soldier extends Chess {
  protected rule(targetX: number, targetY: number): boolean {
    return true;
  }

  get name() {
    return "兵";
  }
}
class King extends Chess {
  name: string = "将";

  protected rule(targetX: number, targetY: number): boolean {
    throw new Error("Method not implemented.");
  }
}

静态成员

什么是静态成员

静态成员是指,附着在类上的成员(属于某个构造函数的成员)

使用 static 修饰的成员,是静态成员

实例成员:对象成员,属于某个类的对象

静态成员:非实例成员,属于某个类

class User {
  constructor(
    public loginId: string,
    public loginPwd: string,
    public name: string,
    public age: number
  ) {}
  static login(loginId: string, loginPwd: string): User | undefined {
    return undefined;
  }
}

静态方法中的 this

实例方法中的 this 指向的是当前对象

而静态方法中的 this 指向的是当前类

class User {
  static users: User[] = [];

  constructor(
    public loginId: string,
    public loginPwd: string,
    public name: string,
    public age: number
  ) {
    //需要将新建的用户加入到数组中
    User.users.push(this);
  }

  sayHello() {
    console.log(
      `大家好,我叫${this.name},今年${this.age}岁了,我的账号是${this.loginId}`
    );
  }

  static login(loginId: string, loginPwd: string): User | undefined {
    return this.users.find(
      (u) => u.loginId === loginId && u.loginPwd === loginPwd
    );
  }
}

new User("u1", "123", "王富贵", 11);
new User("u2", "123", "坤坤", 18);
new User("u3", "123", "旺财", 22);

const result = User.login("u3", "123");
if (result) {
  result.sayHello();
} else {
  console.log("登录失败,账号或密码不正确");
}

设计模式 - 单例模式

单例模式:某些类的对象,在系统中最多只能有一个,为了避免开发者造成随意创建多个类对象的错误,可以使用单例模式进行强约束。

class Board {
  width: number = 500;
  height: number = 700;

  init() {
    console.log("初始化棋盘");
  }

  private constructor() {} //构造函数私有,只能在内部创建对象

  private static _board;

  static createBoard(): Board {
    if (this._board) {
      return this._board;
    }
    this._board = new Board();
    return this._board;
  }
}

const b1 = Board.createBoard(); //静态方法创建
const b2 = Board.createBoard();
console.log(b1 === b2);

再谈接口

接口用于约束类、对象、函数,是一个类型契约。
有一个马戏团,马戏团中有很多动物,包括:狮子、老虎、猴子、狗,这些动物都具有共同的特征:名字、年龄、种类名称,还包含一个共同的方法:打招呼,它们各自有各自的技能,技能是可以通过训练改变的。狮子和老虎能进行火圈表演,猴子能进行平衡表演,狗能进行智慧表演
马戏团中有以下常见的技能:

  • 火圈表演:单火圈、双火圈
  • 平衡表演:独木桥、走钢丝
  • 智慧表演:算术题、跳舞

不适用接口实现时:

  • 对能力(成员函数)没有强约束力
  • 容易将类型和能力耦合在一起

系统中缺少对能力的定义 —— 接口
面向对象领域中的接口的语义:表达了某个类是否拥有某种能力
某个类具有某种能力,其实,就是实现了某种接口
类型保护函数:通过调用该函数,会触发 TS 的类型保护,该函数必须返回 boolean
接口和类型别名的最大区别:接口可以被类实现,而类型别名不可以

接口可以继承类,表示该类的所有成员都在接口中。

索引器

对象[值],使用成员表达式

在 TS 中,默认情况下,不对索引器(成员表达式)做严格的类型检查

使用配置noImplicitAny开启对隐式 any 的检查。

隐式 any:TS 根据实际情况推导出的 any 类型

class User {
  [prop: string]: any; //属性名是字符串,类型是any就行
  constructor(public name: string, public age: number) {}
  sayHello() {}
}
const u = new User("aa", 22);
u.pid = "samoia";

在索引器中,键的类型可以是字符串,也可以是数字

class MyArray {
  [index: number]: string;
  0 = "asdas";
  1 = "sdas";
  2 = "asds";
}
const my = new MyArray();
// my[0]
// my[5] = 'a'

在类中,索引器书写的位置应该是所有成员之前

TS 中索引器的作用

  • 在严格的检查下,可以实现为类动态增加成员
  • 可以实现动态的操作类成员

在 JS 中,所有的成员名本质上,都是字符串,如果使用数字作为成员名,会自动转换为字符串。

在 TS 中,如果某个类中使用了两种类型的索引器,要求两种索引器的值类型必须匹配

class A {
  [prop: number]: string;
  [prop: string]: string;
}
const a = new A();
a[0] = "as";
a["scads"] = "asa";

this 指向约束

https://yehudakatz.com/2011/08/10/understanding-javascript-function-invocation-and-this/

在 JS 中 this 指向的几种情况

明确:大部分时候,this 的指向取决于函数的调用方式

  • 如果直接调用函数(全局调用),this 指向全局对象或 undefined (启用严格模式)
  • 如果使用对象.方法调用,this 指向对象本身
  • 如果是 dom 事件的处理函数,this 指向事件处理对象

特殊情况:

  • 箭头函数,this 在函数声明时确定指向,指向函数位置的 this
  • 使用 bind、apply、call 手动绑定 this 对象

TS 中的 this

配置noImplicitThis为 true,表示不允许 this隐式的指向 any

在 TS 中,允许在书写函数时,手动声明该函数中 this 的指向,将 this 作为函数的第一个参数,该参数只用于约束 this,并不是真正的参数,也不会出现在编译结果中。

// interface IUser {
//     name: string,
//     age: number,
//     sayHello(this: IUser): void
// }

// const u: IUser = {
//     name: "ssf",
//     age: 33,
//     sayHello() {
//         console.log(this.name, this.age)
//     }
// }
// const say = u.sayHello;

class User {
  constructor(public name: string, public age: number) {}

  sayHello() {
    // 类里面的this使用了严格模式,全局调用this指向undefined
    // this指向用户对象本身
    console.log(this, this.name, this.age);
  }
}

装饰器

概述

面向对象的概念(java:注解,c#:特征),decorator
angular 大量使用,react 中也会用到
目前 JS 支持装饰器,目前处于建议征集的第二阶段

解决的问题

装饰器,能够带来额外的信息量,可以达到分离关注点的目的。

  • 信息书写位置的问题
  • 重复代码的问题

上述两个问题产生的根源:某些信息,在定义时,能够附加的信息量有限。

装饰器的作用:为某些属性、类、参数、方法提供元数据信息(metadata)

元数据:描述数据的数据

装饰器的本质

在 JS 中,装饰器是一个函数。(装饰器是要参与运行的

装饰器可以修饰:

  • 成员(属性+方法)
  • 参数
class User {
  // @require
  // @range(3, 5)
  // @description('账号')
  loginId: string; //必须是3-5个字符
  loginPwd: string; //必须是6-12位字符
  age: number; //必须是数字0-100
  gender: "男" | "女";
}
class Article {
  title: string;
}
/**
 * 统一的验证函数
 * @param obj
 */
function validate(obj: object) {
  for (const key in obj) {
    const val = (obj as any)[key];
    // 缺少该属性的验证规则
  }
}

类装饰器

类装饰器的本质是一个函数,该函数接收一个参数,表示类本身(构造函数本身)

使用装饰器**@得到一个函数**

在 TS 中,如何约束一个变量为类

  • Function
  • **new (参数)=>object**

在 TS 中要使用装饰器,需要开启experimentalDecorators

装饰器函数的运行时间:在类定义后直接运行

类装饰器可以具有的返回值

  • void:仅运行函数
  • 返回一个新的类:会将新的类替换掉装饰目标

function test(target: new () => object) {
  return class B {
    // 	不建议这样做
  };
}
@test
class A {}
const a = new A();
console.log(a); //B{}
function test(target: new () => object) {
  return class B extends target {
    // 不建议这样做
  };
}
@test
class A {
  prop1: string; //失去了类型检查
}
const a = new A();
console.log(a); //B{}
function test(target: new (...args: any[]) => object) {}
@test
class A {
  prop1: string;
  constructor(public prop2: string, public prop3: string) {} //默认不能有参数,加上剩余参数能有参数了
}
function test(str: string) {
  return function (target: new (...args: any[]) => object) {};
}
@test("这是一个类") //要满足返回一个函数
class A {
  prop1: string;
}

多个装饰器的情况:会按照后加入先调用的顺序进行调用。

type constructor = new (...args: any[]) => object;
function d1(target: constructor) {
  console.log("d1");
}
function d2(target: constructor) {
  console.log("d2");
}
@d1
@d2
//先输出d2 后 d1
class A {
  prop1: string;
}

面试题

type constructor = new (...args: any[]) => object;
function d1() {
  console.log("d1");
  return function (target: constructor) {
    console.log("d1 decorator");
  };
}
function d2() {
  console.log("d2");
  return function (target: constructor) {
    console.log("d2 decorator");
  };
}
@d1() //先运行d1 d2函数,得到的d1 d2装饰器,装饰器从下到上运行
@d2()
class A {
  prop1: string;
}

成员装饰器

  • 属性

属性装饰器也是一个函数,该函数需要两个参数:

  1. 如果是静态属性,则为类本身;如果是实例属性,则为类的原型
  2. 固定为一个字符串,表示属性名

实例属性

type constructor = new (...args: any[]) => object;
function d(target: any, key: string) {
  // console.log(target === A.prototype, key)
  if (!target.__props) {
    target.__props = [];
  }
  target.__props.push(key);
}
class A {
  @d
  prop1: string;
  @d
  prop2: string;
}
// console.log((A.prototype as any).__props)
const a = new A();
console.log((a as any).__props);

静态属性

function d(target: any, key: string) {
  console.log(target, key);
}
class A {
  @d
  prop1: string;
  @d
  static prop2: string;
}
  • 方法

方法装饰器也是一个函数,该函数需要三个参数:

  1. 如果是静态方法,则为类本身;如果是实例方法,则为类的原型;
  2. 固定为一个字符串,表示方法名
  3. 属性描述对象
function d() {
  return function (target: any, key: string, descriptor: PropertyDescriptor) {
    // console.log(target, key, descriptor)
    descriptor.enumerable = true; //可以更改属性描述符
  };
}
class A {
  @d()
  method1() {}
}

可以有多个装饰器修饰

练习:类和属性的描述装饰器

reflect-metadata 库

该库的作用:保存元数据

import "reflect-metadata";
@Reflect.metadata("a", "一个类")
class A {
  @Reflect.metadata("prop", "一个属性")
  prop1: string;
}
const obj = new A();
console.log(Reflect.getMetadata("a", A));
console.log(Reflect.getMetadata("prop", obj, "prop1"));

class-validator 和 class-transformer 库

import "reflect-metadata";
import {
  IsNotEmpty,
  validate,
  MinLength,
  MaxLength,
  Min,
  Max,
} from "class-validator";

class RegUser {
  @IsNotEmpty({ message: "账号不可以为空" })
  @MinLength(5, { message: "账号必须至少有5个字符" })
  @MaxLength(12, { message: "账号最多12个字符" })
  loginId: string;

  loginPwd: string;

  @Min(0, { message: "年龄的最小值是0" })
  @Max(100, { message: "年龄的最大值是100" })
  age: number;
  gender: "男" | "女";
}

const post = new RegUser();
post.loginId = "22";
post.age = -1;

validate(post).then((errors) => {
  console.log(errors);
});
import "reflect-metadata";
import { plainToClass, Type } from "class-transformer";
import axios from "axios";

class User {
  id: number;
  firstName: string;
  lastName: string;

  @Type(() => Number) //告诉他age是数字
  age: number;

  getName() {
    return this.firstName + " " + this.lastName;
  }

  isAdult() {
    return this.age > 36 && this.age < 60;
  }
}

axios
  .get("https://api.myjson.com/bins/1b59tw")
  .then((resp) => resp.data)
  .then((users) => {
    const us = plainToClass(User, users);
    // console.log(us.getName(), us.isAdult())
    for (const u of us) {
      console.log(typeof u.age, u.age);
    }
  });

补充

  • 参数装饰器

依赖注入、依赖倒置

要求函数有三个参数:

  1. 如果方法是静态的,则为类本身;如果方法是实例方法,则为类的原型
  2. 方法名称
  3. 在参数列表中的索引
  • 关于 TS 自动注入的元数据

如果安装了reflect-metadata,并且导入了该库,并且在某个成员上添加了元数据,并且启用了emitDecoratorMetadata

则 TS 在编译结果中,会将约束的类型,作为元数据加入到相应位置

这样一来,TS 的类型检查(约束)将有机会在运行时进行。

  • AOP(aspect oriented programming)

编程方式,属于面向对象开发。

将一些在业务中共同出现的功能块,横向切分,已达到分离关注点的目的。

class RegUser {
  loginId: string;

  loginPwd: string;
  // @规则
  age: number;
  // @规则
  pid: string;

  email: string;

  /**
   * 将用户保存到数据库
   */
  save() {
    // 验证抽离出去了
    if (validate(this)) {
      //通过后保存数据库
    }
  }
}

类型演算

根据已知的信息,计算出新的类型

三个关键字

  • typeof

TS 中的 typeof,书写的位置在类型约束的位置上。

表示:获取某个数据的类型

const a: string = "asnd";
let b: typeof a = "ad";
console.log(typeof b);

当 typeof 作用于类的时候,得到的类型,是该类的构造函数

// 当想要约束参数为构造函数的时候:
class User {
  loginId: string;
  loginPwd: string;
}
// 方法1
// function createUser(cls: new () => User): User {
//     return new cls()
// }
// 方法2
function createUser(cls: typeof User): User {
  return new cls();
}
const u = createUser(User);
  • keyof

作用于类、接口、类型别名,用于获取其他类型中的所有成员名组成的联合类型

interface User {
  loginId: string;
  loginPwd: string;
  age: number;
}
// function printUserProperty(obj: User, prop: string) {
//     console.log(obj[prop])//报错,因为不确定prop是loginId,loginPwd,age之一
// }
function printUserProperty(obj: User, prop: "loginId" | "loginPwd" | "age") {
  console.log(obj[prop]);
}
const u: User = {
  loginId: "sada",
  loginPwd: "sss",
  age: 22,
};
printUserProperty(u, "age");

// 场景:某个类型应该是某个类型所有字段中的一个

应用 keyof

interface User {
  loginId: string;
  loginPwd: string;
  age: number;
}
function printUserProperty(obj: User, prop: keyof User) {
  console.log(obj[prop]);
}
const u: User = {
  loginId: "sada",
  loginPwd: "sss",
  age: 22,
};
printUserProperty(u, "age");
  • in

该关键字往往和 keyof 联用,限制某个索引类型的取值范围。

interface User {
  loginId: string;
  loginPwd: string;
  age: number;
}
type Obj = {
  // [p:string]:string//太宽泛,什么属性都可以加

  [p in "loginId" | "loginPwd" | "age"]: string; //  []叫索引器
  /**
   * 等价于
   * loginId:string
   * loginPwd:string
   * age:string
   */
};
const u: Obj = {
  age: "1",
  loginId: "a",
  loginPwd: "aa",
};
// u.abc = '123'
// 只能加"loginId" | "loginPwd" | "age"中的一个
u.age = "1";

用 keyof 简化

interface User {
  loginId: string;
  loginPwd: string;
  age: number;
}
// 类型演算:将User所有类型变成字符串,得到一个新类型
type Obj = {
  [p in keyof User]: string;
};
const u: Obj = {
  age: "1",
  loginId: "a",
  loginPwd: "aa",
};
u.age = "1";
interface User {
  loginId: string;
  loginPwd: string;
  age: number;
}
type Obj = {
  [p in keyof User]: User[p]; //不改变User里面的的类型,直接取出来附上
};
const u: Obj = {
  age: 1,
  loginId: "a",
  loginPwd: "aa",
};
u.age = 1;
type Obj = {
  readonly [p in keyof User]: User[p]; //类型不变,全部变成只读
};

配合泛型:强大了

import { type } from "os";

interface User {
  loginId: string;
  loginpwd: string;
}

interface Article {
  title: string;
  publishDate: Date;
}
//将User的所有属性值类型变成字符串,得到一个新类型
type String<T> = {
  [p in keyof T]: string;
};

type Readonly<T> = {
  readonly [p in keyof T]: T[p];
};

type Partial<T> = {
  [p in keyof T]?: T[p];
};

const u: String<Article> = {
  title: "Sfsdf",
  publishDate: "sdf",
};

TS 中预设的类型演算

Partial<T>  // 将类型T中的成员变为可选

Required<T>  // 将类型T中的成员变为必填

Readonly<T> // 将类型T中的成员变为只读

Exclude<T, U> // 从T中剔除可以赋值给U的类型。

Extract<T, U> // 提取T中可以赋值给U的类型。

NonNullable<T> // 从T中剔除null和undefined。

ReturnType<T> // 获取函数返回值类型。

InstanceType<T> // 获取构造函数类型的实例类型。

用法:

interface User {
  age: number;
  name: string;
}
let u: Partial<User>;
u = {
  age: 23,
};
// let u: Exclude<"a" | "b" | "c" | "d", "b" | "c">

type T = "男" | "女" | null | undefined;
type NEWT = Exclude<T, null | undefined>;

type func = () => number;
type returnType = ReturnType<func>; //传入的是函数类型

function sum(a: number, b: number) {
  return a + b;
}
let a: ReturnType<typeof sum>; //传入的是函数类型

声明文件

概述、编写、发布

概述

  1. 什么是声明文件?

.d.ts结尾的文件

  1. 声明文件有什么作用?

为 JS 代码提供类型声明

  1. 声明文件的位置
  • 放置到 tsconfig.json 配置中包含的目录中
  • 放置到 node_modules/@types 文件夹中
  • 手动配置
  • 与 JS 代码所在目录相同,并且文件名也相同的文件。用 ts 代码书写的工程发布之后的格式。

编写声明文件

手动编写   自动生成

  • 自动生成

工程是使用 ts 开发的,发布(编译)之后,是 js 文件,发布的是 js 文件。

如果发布的文件,需要其他开发者使用,可以使用声明文件,来描述发布结果中的类型。

配置tsconfig.json中的declaration:true即可

  • 手动编写
  1. 对已有库,它是使用 js 书写而成,并且更改该库的代码为 ts 成本较高,可以手动编写声明文件
  2. 对一些第三方库,它们使用 js 书写而成,并且这些第三方库没有提供声明文件,可以手动编写声明文件。

全局声明

声明一些全局的对象、属性、变量

namespace: 表示命名空间,可以将其认为是一个对象,命名空间中的内容,必须通过命名空间.成员名访问

模块声明

三斜线指令

在一个声明文件中,包含另一个声明文件

发布

  1. 当前工程使用 ts 开发

编译完成后,将编译结果所在文件夹直接发布到 npm 上即可

  1. 为其他第三方库开发的声明文件

发布到@types/**中。

1) 进入 github 的开源项目:https://github.com/DefinitelyTyped/DefinitelyTyped

2) fork 到自己的开源库中

3) 从自己的开源库中克隆到本地

4) 本地新建分支(例如:mylodash4.3),在新分支中进行声明文件的开发

在types目录中新建文件夹,在新的文件夹中开发声明文件

5) push 分支到你的开源库

6) 到官方的开源库中,提交 pull request

7) 等待官方管理员审核(1 天)

审核通过之后,会将你的分支代码合并到主分支,然后发布到 npm。

之后,就可以通过命令npm install @types/你发布的库名


文章作者: Sunny
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Sunny !
  目录