五、TypeScript基础
大约 35 分钟约 10462 字
(一)概述
1.为什么要学习 TypeScript
- 就业
- 获得更大的竞争优势
- 获得更好的开发体验
- 解决 JS 中一些难以处理的问题
2.JS 开发中的问题
- 使用了不存在的变量、函数或成员
- 把一个不确定的类型当作一个确定的类型处理
- 使用 null 或 undefined 的成员
// 有错误的代码
function getUserName() {
if (Math.random() < 0.5) {
return "yuan jin";
}
return 404;
}
let myname = getUsername();
mynema = myname
.split(" ")
.filter((it) => it)
.map((it) => it[0].toupperCase() + it.substr(1))
.join(" ");
3.JS 的原罪
- JS 语言本身的特性
- 决定了该语言无法适应大型的复杂的项目
- 弱类型语言
- 某个变量可以随时更换类型
- 解释性语言
- 错误发生在运行时
4.TypeScript 简介
- 简称 TS,是 JS 的超集
- 是一个可选的、静态的类型系统
1)超集
- 类比整数和正整数
- 整数是正整数的超集
2)类型系统
- 对代码中所有的标识符(变量、函数、参数、返回值)进行类型检查
- 要构建大型的应用,会涉及大量的函数和接口
- 如果没有类型检查,会产生大量的调试成本
- 类型系统可以降低调试成本,从而降低开发成本
function getUserName(): string | number {
if (Math.random() < 0.5) {
return "yuan jin";
}
return 404;
}
let myName = getUserName();
if (typeof myName === "string") {
myName = myName
.split(" ")
.filter((it) => it)
.map((it) => it[0].toUpperCase() + it.substring(1))
.join(" ");
}
3)可选的
- 学习曲线非常平滑
- JS 的所有功能都能够在 TS 中使用
- 增加的部分是类型系统
4)静态的
- 在运行之前 是静态的
- 无论是浏览器环境,还是 Node 环境,都无法直接识别 ts 代码
- babel: es6 -> es5
- tsc: ts -> es
- tsc
- TS 编译器
- TS 代码 -> 编译 -> JS 代码
- 静态
- 类型检查发生的时间,是在 编译时 而非运行时
- TS 不参与任何运行时的类型检查
- 运行的是编译后的 JS 代码
5)TS 的常识
- 2012 年微软发布(ES6/ES2015)
- Anders Hejlsberg 负责开发 TS 项目
- 开源、拥抱 ES 标准
6)额外的惊喜
- 有了类型检查,增强了面向对象的开发
- JS 中也有类和对象,支持面向对象开发
- 但是没有类型检查,很多面向对象的场景实现起来有诸多问题
- 使用 TS 后,可以编写出完善的面向对象代码
(二)在 Node 中搭建 TS 开发环境
1.安装 TypeScript
npm i -g typescript
1)默认编译选项
- 假设当前的执行环境是 DOM(浏览器环境)
- 如果代码中没有使用模块化语句(import、export),则认为该代码是全局执行
- 编译的目标代码是 ES3,发挥最大的兼容性
2)更改编译选项
- 方式一:使用 tsc 命令行时加上选项参数
tsc index.ts
- 方式二:使用 ts 配置文件更改编译选项
tsc --init
2.TS 的配置文件
// tsconfig.json
{
"compilerOptions": {
// 编译选项
"target": "es2016", // 配置编译目标代码的版本标准
"module": "commonjs", // 配置编译目标使用的模块化标准
"lib": ["es2016"],
"outDir": "./dist"
},
"include": ["./src"]
}
- 如果使用配置文件,则在使用 tsc 进行编译时不能跟上文件名
tsc
let abc = "asdf";
- 如果跟上文件名,会忽略配置文件
tsc ./src/index.ts
var abc = "asdf";
@types/node
3.- @types 是一个 TS 官方的类型库
- 包含了很多对 JS 代码的类型描述
- 如:JQuery 库是用 JS 写的,没有类型检查
- 安装
@types/jquery
可以为 JQuery 库添加类型定义
- 安装
npm i -g -D @types/node
4.使用第三方库简化流程
// package.json
{
"scripts": {
"dev": "nodemon --watch src -e ts --exec ts-node src/index.ts"
},
"devDependencies": {
"@types/node": "^11.13.7"
}
}
1)ts-node
- 将 TS 代码在内存中完成编译,同时完成运行
- 不生成编译后的 JS 文件
npm i -g ts-node
ts-node src/index.ts
2)nodemon
- 检测文件的变化
npm i -g nodemon
ts-node src/index.ts
3)流程
- 开发过程中使用
npm run dev
编译运行 TS 文件 - 开发完成后使用
tsc
打包输出 dist 目录下的 JS 文件
(三)基本类型检查
1.基本类型约束
1)类型约束
- 仅需要在变量、函数的参数、函数的返回值位置加上
:类型
let name: string;
name = "333";
function sum(a: number, b: number): number {
return a + b;
}
let num: number = sum(3, 4);
- TS 在很多场景中可以完成类型推导
// 智能推导出返回值为 number 类型
function sum(a: number, b: number) {
return a + b;
}
let num = sum(3, 4);
- 如果推导不出类型,自动设置为 any 类型
- 表示任意类型,TS 不对该类型进行类型检查
let age;
如何区分数字字符串和数字
- 关键看怎么读
- 如果按照数字的方式朗读,则为数字
- 如:
¥100.00
读作一百元
,是数字 let price: number = 100.00
- 如:
- 否则,为字符串
- 如:
138****8085
读作一三八****八零八五
,是字符串 let phone: string = "138****8085"
- 如:
2)源代码和编译结果的差异
- 编译结果中没有类型约束信息
let name;
name = "333";
function sum(a, b) {
return a + b;
}
let num = sum(3, 4);
2.基本类型
类型 | 含义 |
---|---|
number | 数字 |
string | 字符串 |
boolean | 布尔 |
array | 数组 必须指定每一项的类型 |
object | 对象 约束力不强,无法约束每一个属性的类型 |
null 和 undefined | 是其他类型的子类型 可以赋值给其他类型 |
function isOdd(n: number) {
return n % 2 === 0;
}
let nums: number[] = [3, 4, 5]; // 语法糖
let nums2: Array<number> = [3, 4, 5];
function printValues(obj: object) {
const values = Object.values(obj);
values.forEach((v) => console.log(v));
}
printValues({
name: "afd",
age: 33,
});
let n: string = undefined;
n.toUpperCase();
- 配置
strictNullChecks: true
可以获得更严格的空类型检查 - 此时 null 和 undefined 只能赋值给自身
{
"compilerOptions": {
"strictNullChecks": true
}
}
3.其他常用类型
1)联合类型
- 多种类型任选其一
- 配合类型保护进行判断
- 类型保护
- 当对某个变量进行类型判断后,在判断的语句块中便可以确定它的确切类型
typeof
可以触发 简单类型 的类型保护
let name1: string | undefined;
if (typeof name1 === "string") {
// 类型保护
console.log(name.length);
}
2)void 类型
- 通常用于约束函数的返回值,表示该函数没有任何返回
function printMenu() {
console.log("1. 登录");
console.log("2. 注册");
}
3)never 类型
- 通常用于约束函数的返回值,表示该函数永远不可能结束
function throwError(msg: string): never {
throw new Error(msg);
}
function alwaysDoSomething(): never {
while (true) {
//...
}
}
4)字面量类型
- 使用一个值进行约束
let a: "A";
a = "A";
// a = 'B'; // 报错
let gender: "男" | "女";
gender = "女";
gender = "男";
let arr: []; // arr永远只能取值为一个空数组
- 对象字面量可以更加细化地约束一个对象
- 通常使用接口或类约束对象
let user: {
name: string;
age: number;
};
user = {
name: "34",
age: 33,
};
5)元祖类型(Tuple)
- 一个固定长度的数组,并且数组中每一项的类型确定
let tu: [string, number];
tu = ["3", 4];
6)any 类型
- any 类型可以绕过类型检查
- 因此,any 类型的数据可以赋值给任意类型
- 不要随意使用 any
let data: any = "sfdsdf";
let num3: number = data;
4.类型别名
- 对已知的一些类型定义名称
type 类型名 = ...
type Gender = "男" | "女";
type User = {
name: string;
age: number;
gender: Gender;
};
let u: User;
u = {
name: "sdfd",
gender: "男",
age: 34,
};
function getUsers(g: Gender): User[] {
return [];
}
getUsers("女");
5.函数的相关约束
1)函数重载
- 在函数实现之前,对函数调用的多种情况进行声明
// 声明不同类型参数的返回值类型,确保result类型【函数重载】
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("a", "b");
const result2 = combine(1, 2);
console.log(result1, result2);
2)可选参数
- 可以在某些参数名后加上问号,表示该参数可以不用传递
- 可选参数必须在参数列表的末尾
- 默认参数一定是可选参数
// function sum3(a: number, b: number, c: number = 0) {
function sum3(a: number, b: number, c?: number) {
if (c) {
return a + b + c;
} else {
return a + b;
}
}
sum3(3, 4);
sum3(3, 4, 5);
6.案例实操:创建并打印扑克牌
- 创建一副扑克牌(不包括大小王),打印该扑克牌
// 扑克牌花色的类型
type Color = "♥" | "♠" | "♦" | "♣";
// 一张扑克牌的类型
type NormalCard = {
color: Color;
mark: number;
};
// 一副扑克牌的类型
type Deck = NormalCard[];
/**
* 创建一副扑克牌
*/
const createDeck = (): Deck => {
const deck: Deck = [];
for (let i = 1; i <= 13; i++) {
deck.push({
mark: i,
color: "♠",
});
deck.push({
mark: i,
color: "♣",
});
deck.push({
mark: i,
color: "♥",
});
deck.push({
mark: i,
color: "♦",
});
}
return deck;
};
/**
* 打印扑克牌
*/
const printDeck = (deck: Deck) => {
let result = "\n";
deck.forEach((card, i) => {
let str = card.color;
if (card.mark <= 10) {
str += card.mark;
} else if (card.mark === 11) {
str += "J";
} else if (card.mark === 12) {
str += "Q";
} else {
str += "K";
}
result += str + "\t";
if ((i + 1) % 6 === 0) {
result += "\n";
}
});
console.log(result);
};
const deck = createDeck();
printDeck(deck);
(四)扩展类型 —— 枚举
- 枚举通常用于约束某个变量、参数、函数返回值的取值范围
1.扩展类型
- 包括类型别名、枚举、接口、类
- 类型别名、接口,不产生编译结果
- 枚举、类,产生编译结果
- 枚举 -> 编译 -> 对象
- TS 类 -> 编译 -> JS 类
2.字面量类型的问题
- 字面量和联合类型配合使用,也可以约束取值范围
let gender: "男" | "女";
1)在类型约束位置,会产生重复代码
- 可以使用类型别名解决该问题
let gender: "男" | "女";
function searchUsers(g: "男" | "女") {}
2)逻辑含义和真实的值产生混淆
- 会导致当修改真实值的时候,产生大量的修改
// type Gender = "男" | "女";
type Gender = "male" | "female";
// 使用真实值时都要一起修改
let gender: Gender;
gender = "男";
gender = "女";
3)字面量类型不会进入到编译结果
- 无法动态读取字面量类型信息再显示到页面上
3.定义枚举
/*
enum 枚举名{
枚举字段1 = 值1,
枚举字段2 = 值2,
...
}
*/
enum Gender {
male = "男",
female = "女",
}
// 使用逻辑含义赋值
let gender: Gender;
gender = Gender.male;
gender = Gender.female;
- 枚举会出现在编译结果中
- 编译结果中表现为对象
var Gender;
(function (Gender) {
Gender["male"] = "\u7537";
Gender["female"] = "\u5973";
})(Gender || (Gender = {}));
// 使用逻辑含义赋值
let gender;
gender = Gender.male;
gender = Gender.female;
4.枚举的规则
- 枚举的字段值可以是字符串或数字
- 数字枚举的值会自动自增
- 被数字枚举约束的变量,可以直接赋值为数字
enum Level {
level1, // 0
level2, // 1
level3, // 2
}
let l: Level = Level.level1;
l = Level.level2;
// l = 2; // 不报错,不推荐
- 数字枚举的编译结果和字符串枚举有差异
- 如果要遍历输出需要考虑相关结果
var Level;
(function (Level) {
Level[(Level["level1"] = 0)] = "level1";
Level[(Level["level2"] = 1)] = "level2";
Level[(Level["level3"] = 2)] = "level3";
})(Level || (Level = {}));
let l = Level.level1;
l = Level.level2;
/*
{
level1: 0,
level2: 1,
level3: 2,
0: "level1",
1: "level2",
2: "level3"
}
*/
最佳实践
- 尽量不要在一个枚举中既出现字符串字段,又出现数字字段
- 使用枚举时,尽量使用枚举字段的名称,而不使用真实的值
5.案例实操:使用枚举优化扑克牌
- 使用枚举改造程序
// 扑克牌花色的类型
enum Color {
heart = "♥",
spade = "♠",
club = "♣",
diamond = "♦",
}
// 扑克牌数字的类型
enum Mark {
A = "A",
two = "2",
three = "3",
four = "4",
five = "5",
six = "6",
seven = "7",
eight = "8",
nine = "9",
ten = "10",
eleven = "J",
twelve = "Q",
king = "K",
}
// 一张扑克牌的类型
type NormalCard = {
color: Color;
mark: Mark;
};
// 一副扑克牌的类型
type Deck = NormalCard[];
/**
* 创建一副扑克牌
*/
const createDeck = (): Deck => {
const deck: Deck = [];
const marks = Object.values(Mark);
const colors = Object.values(Color);
for (const m of marks) {
for (const c of colors) {
deck.push({
color: c,
mark: m,
});
}
}
return deck;
};
/**
* 打印扑克牌
*/
const printDeck = (deck: Deck) => {
let result = "\n";
deck.forEach((card, i) => {
let str = card.color + card.mark;
result += str + "\t";
if ((i + 1) % 6 === 0) {
result += "\n";
}
});
console.log(result);
};
const deck = createDeck();
printDeck(deck);
6.位枚举【扩展】
- 枚举的位运算
- 针对数字枚举
1)位运算
- 两个数字换算成二进制后进行的运算
2)枚举的组合
- 需求:将多个枚举组合成新的枚举值
// // 无法快速穷举
// enum Permission {
// Read,
// Write,
// Create,
// Delete,
// ReadAndWrite,
// ReadAndWriteAndCreate,
// // ...
// }
// 哪一位是1,表示有该位对应的权限
enum Permission {
Read = 1, // 0001 2^0
Write = 2, // 0010 2^1
Create = 4, // 0100 2^2
Delete = 8, // 1000 2^3
}
- 使用或运算组合权限
- 有 1 为 1
/**
* 0001
* 或
* 0010
* =>
* 0011
*/
let p: Permission = Permission.Read | Permission.Write;
p = p | Permission.Delete;
2)判断是否拥有某种权限
- 使用与运算判断权限
- 全 1 为 1
/**
* 0011
* 且
* 0010
* =>
* 0010
*/
function hasPermission(target: Permission, per: Permission) {
return (target & per) === per;
}
//判断变量p是否拥有可读权限
console.log(hasPermission(p, Permission.Read));
3)删除某种权限
- 使用异或运算删除权限
- 不同为 1
/**
* 0011
* 异或
* 0010
* =>
* 0001
*/
p = p ^ Permission.Write;
console.log(hasPermission(p, Permission.Write));
(五)TS 中的模块化
前端领域中的模块化标准
ES6、CommonJS、AMD、UMD、System、ESNext
- 在 TS 中导入和导出模块,统一使用 ES6 的模块化标准
1.ES 模块化代码
1)普通导出【推荐】
- 导入的文件直接使用导出变量,会自动导入
- 因为 TS 不允许为定义变量存在,会自动从模块中寻找
// 导出
export const name = "kevin";
export function sum(a: number, b: number) {
return a + b;
}
// 导入
import { name, sum } from "./myModule";
2)默认导出
- 导入的文件直接使用变量,不会自动导入
- 因为默认导出是 default 对象,没有固定的名称
- 具体使用的模块名称需要在导入时指定,所以没有智能提示
// 导出
export default {
name: "kevin",
sum(a: number, b: number) {
return a + b;
},
};
// 导入
import myModule from "./myModule";
2.编译结果中的模块化
# 编译并监听文件变化,不执行文件
tsc --watch
1)模块化可配置
配置名称 | 含义 |
---|---|
module | 设置编译结果中使用的模块化标准 |
moduleResolution | 设置解析模块的模式 |
noImplicitUseStrict | 编译结果中不包含 "use strict" |
removeComments | 编译结果移除注释 |
noEmitOnError | 错误时不生成编译结果 |
esModuleInterop | 启用 ES 模块化 和 非 ES 模块导出 交互 |
{
"compilerOptions": {
// 编译选项
"target": "es2016", // 配置编译目标代码的版本标准
// "module": "es6",
"module": "commonjs", // 配置编译目标使用的模块化标准
"lib": ["es2016"],
"outDir": "./src/dist",
"strictNullChecks": true,
"removeComments": true,
"noImplicitUseStrict": true,
"esModuleInterop": true,
"noEmitOnError": true,
"moduleResolution": "node"
},
"include": ["./src"]
}
2)编译结果的模块化标准是 ES6
- 编译结果和书写的代码没有区别
3)编译结果的模块化标准是 CommonJS
- 导出的声明会变成 exports 的属性
- 默认导出会变成 exports 的 default 属性
export const name = "kevin";
export function sum(a: number, b: number) {
return a + b;
}
export default function () {
console.log("hello my module!");
}
"use strict";
// 相当于 exports.__esModule = true;
Object.defineProperty(exports, "__esModule", { value: true });
exports.sum = exports.name = void 0;
exports.name = "kevin";
function sum(a, b) {
return a + b;
}
exports.sum = sum;
function default_1() {
console.log("hello my module!");
}
exports.default = default_1;
/*
相当于
module.exports = {
name: "kevin",
sum(a, b) {
return a + b;
},
};
*/
- 导入的编译结果
import sayHello, { name, sum } from "./myModule";
console.log(name);
sayHello();
Object.defineProperty(exports, "__esModule", { value: true });
const myModule_1 = require("./myModule");
console.log(myModule_1.name);
(0, myModule_1.default)();
4)导入的模块不是 CommonJS 的导出格式
- 如果导入了不是用 commonjs 导出的模块
- 编译结果会调用 default 属性,导致报错
- 配置
"esModuleInterop": true
也可以解决
// fs模块的导出方式是 module.exports = {}
import fs from "fs";
fs.readFileSync("./");
const fs_1 = require("fs");
fs_1.default.readFileSync("./");
- 解决
import { readFileSync } from "fs";
readFileSync("./");
// 或者
import * as fs from "fs";
fs.readFileSync("./");
const fs_1 = require("fs");
(0, fs_1.readFileSync)("./");
// 或者
const fs = require("fs");
fs.readFileSync("./");
3.CommonJS 模块化代码
1)可以直接使用原有方式书写
- 无法获得类型检查
// 导出
module.exports = {
name: "kevin",
sum(a: number, b: number) {
return a + b;
},
};
// 导入
const myModuleCommonJS = require("./myModule");
console.log(myModuleCommonJS);
2)使用 TS 要求的格式书写
// 导出
export = {
name: "kevin",
sum(a: number, b: number) {
return a + b;
},
};
// 导入
import myModule = require("./myModule");
- 编译结果
"use strict";
module.exports = {
name: "kevin",
sum(a, b) {
return a + b;
},
};
4.模块解析
- 应该从什么位置寻找模块
- TS 中,有两种模块解析策略
- classic:经典解析策略
- node:Node 解析策略
- 和 NodeJS 中一致
- 唯一变化是将
js
替换为ts
1)使用相对路径导入
- 先找当前目录下是否有同名文件
- 再找当前项目 package.json 是否配置了 main 字段
- 再找同名目录下的 index.ts 文件
- .ts => .json => .node => .mjs
// 相对路径
require("./xxx");
2)导入非相对模块
- 先找当前项目的 node_modules 目录下是否有同名模块
- 再往上级的 node_modules 目录寻找
// 非相对模块
require("xxx");
强制开启 node 解析策略
"moduleResolution": "node"
5.案例实操:使用模块化优化扑克牌
1)使用模块化
import { createDeck, printDeck } from "./utils";
import { Color, Mark } from "./enums";
import { Deck } from "./types";
2)编译结果
- types 文件仅导出需要的类型
- 编译后的 js 不需要这些类型
- 所以编译结果为空,并且使用该类型的文件没有任何导入
// types.js
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
3)重置编译文件输出目录
- 每次重新编译打包后,先删除 dist 目录,再输出打包文件
- 配置命令
rd
:remove directory/s
:删除文件夹及文件夹内的内容/q
:后续所有是否问题全部确认 y
{
"scripts": {
"build": "rd /s /q dist & tsc"
}
}
(六)接口和类型兼容性
1.扩展类型 —— 接口 interface
1)TypeScript 的接口
- 用于约束类、对象、函数的契约(标准)
- 和 type 类型别名一样,接口 不出现在编译结果中
- 和 type 定义的约束没有区别
- 只在 约束类 的时候两者才有区别
- 推荐使用 interface 约束
2)契约(标准)的形式
- API 文档,弱标准
- 代码约束,强标准
2.接口的使用
1)使用接口约束对象
interface User {
name: string;
age: number;
// sayHello: () => void;
sayHello(): void; // 对象内的函数 —— 方法
}
// // 使用type约束
// type User = {
// name: string;
// age: number;
// sayHello: () => void;
// };
let u: User = {
name: "sdfds",
age: 33,
sayHello() {
console.log("asfadasfaf");
},
};
2)使用接口约束函数
{}
是定界符- 不表示对象
- 表示包裹的内容是具体的约束内容
// type Condition = (n: number) => boolean
// type Condition {
// (n: number): boolean;
// }
interface 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, 7, 11], (n) => n % 2 !== 0);
console.log(result);
3.接口的继承
判断继承关系
只要可以被描述为 “A 是一个 B” 就构成继承关系
class Banner extends React.Component {}
1)组合多种接口
interface A {
T1: string;
}
interface B {
T2: number;
}
interface C extends A, B {
T3: boolean;
}
let u: C = {
T2: 33,
T1: "43",
T3: true,
};
2)使用类型别名
- 可以实现类似的组合效果
- 需要通过交叉类型
&
- 连接多个类型
- 结果类型必须包含所有交叉类型的属性和方法
type A = {
T1: string;
};
type B = {
T2: number;
};
type C = {
T3: boolean;
} & A &
B;
let u: C = {
T2: 33,
T1: "43",
T3: true,
};
3)区别
- 子接口不能覆盖父接口的成员
interface A {
T1: string;
}
interface B {
T2: number;
}
interface C extends A, B {
// T1: number; // 报错
T3: boolean;
}
- 交叉类型会把相同成员的类型进行交叉
type A = {
T1: string;
};
type B = {
T2: number;
};
type C = {
T1: number;
T3: boolean;
} & A &
B;
let u: C = {
T2: 33,
// T1: "43", // number & string,可以使用两种类型的方法,但是无法赋值
T3: true,
};
4.readonly
- 只读修饰符,修饰的目标是只读
- 只读修饰符不出现在编译结果中
1)禁止不合理操作
// type User = {
interface User {
readonly id: string;
name: string;
age: number;
}
let u: User = {
id: "123",
name: "Asdf",
age: 33,
};
u.id = "32323"; // 不合理操作,初始化赋值后又尝试修改
2)修饰数组
interface User {
// 第一个表示该属性不能重新赋值,第二个表示数组内容不可改变
readonly arr: readonly string[];
}
let u: User = {
arr: ["Sdf", "dfgdfg"],
};
// const 表示该数组不可重新赋值,readonly 表示该数组内容不可改变
// const arr: ReadonlyArray<number> = [3, 4, 6];
const arr: readonly number[] = [3, 4, 6];
// 修改原数组的函数都无法调用,包括根据索引修改
arr.push();
arr[0] = 3;
5.类型兼容性
- 如果能完成 B->A 的赋值,则 B 和 A 类型兼容
1)总原则:鸭子辨型法
- 又叫子结构辨型法
- 目标类型需要某一些特征,赋值的类型只要能满足该特征即可
- 为了让开发者使用类型时更丝滑
- 比如第三方库的对象有多个属性,但是开发者只需要其中一两个属性
- 有了该判定方法后就不需要约束整个第三方库的对象
2)基本类型
- 赋值的类型需要完全匹配
3)对象类型
- 赋值的类型使用鸭子辨型法判定
- 当直接使用对象字面量赋值时,会进行更加严格的判断
- 类型断言
- 开发者非常清楚某个东西的类型,但是 TS 难以分辨
- 可以通过类型断言
as
告诉 TS 确切类型 数据 as 类型
interface Duck {
sound: "嘎嘎嘎"; // 字面量
swim(): void;
}
let person = {
name: "伪装成鸭子的人",
age: 11,
sound: "嘎嘎嘎" as "嘎嘎嘎",
swim() {
console.log(this.name + "正在游泳,并发出了" + this.sound + "的声音");
},
};
/**
* person中有满足Duck类型的结构,所以可以赋值
* 第三方变量不清楚自定义接口的结构
* 所以赋值时宽松判断,允许不相关字段存在
*/
let duck: Duck = person;
/**
* 直接使用对象字面量赋值,会进行更严格的判断
* 直接书写字面量时一定清楚自定义接口的结构
* 此时赋值严格判断,不允许不相关字段存在
*/
let duck2: Duck = {
// name: "伪装成鸭子的人", // 报错
// age: 11, // 报错
sound: "嘎嘎嘎" as "嘎嘎嘎", // 类型断言
swim() {
console.log(this.name + "正在游泳,并发出了" + this.sound + "的声音");
},
};
4)函数类型
- 传递给目标函数的参数可以少,但不可以多
// 需求1:传递参数n,返回符合条件的数组
// 需求2:传递参数n和i,返回符合条件的数组中第i项
interface Condition {
(n: number, i: number): boolean;
}
function sum(numbers: number[], callBack: Condition) {
let s = 0;
numbers.forEach((n, i) => {
if (callBack(n, i)) {
s += n;
}
});
return s;
}
/**
* 缺少参数i,但是不报错
*/
const result = sum([3, 4, 5, 7, 11], (n) => n % 2 !== 0);
// const result = sum([3, 4, 5, 7, 11], (n, i) => i % 2 !== 0);
console.log(result);
- 要求返回则必须返回,不要求返回则随意
- 不要求返回的函数,说明内部不使用返回值
- 尽管返回了也不会报错
// (value: number, index: number, array: number[]) => void
[34, 4].forEach((it) => console.log(it));
6.案例实操:使用接口优化扑克牌
- 用接口改造程序,加入大小王
1)定义接口
// 一张扑克牌的接口
export interface NormalCard {
color: Color;
mark: Mark;
}
// 大小王的接口
export interface Joker {
type: "big" | "small";
}
// 一副扑克牌的类型
export type Deck = (NormalCard | Joker)[];
- 或者
// 一副扑克牌的类型
export interface Card {
getString(): string;
}
// 一张扑克牌的接口
export interface NormalCard extends Card {
color: Color;
mark: Mark;
}
// 大小王的接口
export interface Joker extends Card {
type: "big" | "small";
}
// 一副扑克牌的类型
export type Deck = Card[];
2)创建扑克牌
- 方法一:使用字面量
export const createDeck = (): Deck => {
const deck: Deck = [];
const marks = Object.values(Mark);
const colors = Object.values(Color);
for (const m of marks) {
for (const c of colors) {
// // 字面量赋值要求更严格的检查
// deck.push({
// color: c,
// mark: m,
// });
const card: NormalCard = {
color: c,
mark: m,
getString() {
return this.color + this.mark;
},
};
deck.push(card);
}
}
return deck;
};
- 方法二:使用类型断言
export const createDeck = (): Deck => {
const deck: Deck = [];
const marks = Object.values(Mark);
const colors = Object.values(Color);
for (const m of marks) {
for (const c of colors) {
// // 写法一
// deck.push({
// color: c,
// mark: m,
// getString() {
// return this.color + this.mark;
// },
// } as Card);
// 写法二
// React 中不推荐,会和组件混淆
deck.push(<Card>{
color: c,
mark: m,
getString() {
return this.color + this.mark;
},
});
}
}
// 加入大小王
let joker: Joker = {
type: "small",
getString() {
return "jo";
},
};
deck.push(joker);
joker = {
type: "big",
getString() {
return "JO";
},
};
deck.push(joker);
return deck;
};
3)打印扑克牌
export const printDeck = (deck: Deck) => {
let result = "\n";
deck.forEach((card, i) => {
// let str = card.color + card.mark;
// result += str + "\t";
result += card.getString() + "\t";
if ((i + 1) % 6 === 0) {
result += "\n";
}
});
console.log(result);
};
(七)TS 中的类
基础部分学习类的时候,仅讨论新增的语法部分
- 面向对象思想
- TS 类
- 属性列表
- 修饰符
- 只读修饰符:readonly
- 访问修饰符:public、private、protected
1.属性
- 要求使用属性列表来描述类中的属性
// // JS写法,报错,声明类时不能在构造函数中动态增加属性
// class User {
// constructor(name: string, age: number) {
// this.name = name;
// this.age = age;
// }
// }
class User {
name: string;
age: number;
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
}
1)属性的初始化检查
- 严格检查属性是否初始化
- 是否有构造函数
- 是否在构造函数中赋值
- 实例化对象时是否传递参数
"strictPropertyInitialization": true
2)属性的初始化位置
- 构造函数中设置默认值
- 属性列表设置默认值
class User {
name: string;
age: number;
gender: "男" | "女" = "男"; // 默认值设置位置2
// 默认值设置位置1
// constructor(name: string, age: number, gender: "男" | "女" = "男") {
// this.name = name;
// this.age = age;
// this.gender = gender;
// }
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
}
3)属性可以修饰为可选的
class User {
name: string;
age: number;
gender: "男" | "女" = "男";
// pid: string | null = null; // null 必须有默认值
// pid: string | undefined;
pid?: string;
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
}
4)属性可以修饰为只读的
class User {
name: string;
age: number;
gender: "男" | "女" = "男";
pid?: string;
readonly id: number; // 初始化之后不可改变
constructor(name: string, age: number) {
this.name = name;
this.age = age;
this.id = Math.random();
}
}
2.访问修饰符
- 可以控制类中的某个成员(属性和方法)的访问权限
- 在 JS 中使用 Symble 实现私有成员
1)分类
修饰符 | 含义 |
---|---|
public | 公开的,所有的代码均可访问【默认】 |
private | 私有的,只有在类中可以访问 |
protected | 受保护的,只有父类和子类可以访问 |
2)属性使用访问修饰符
class User {
name: string;
age: number;
gender: "男" | "女" = "男";
pid?: string;
readonly id: number;
private _publishNumber: number = 3; // 每天一共可以发布多少篇文章
private _curNumber: number = 0; // 当前可以发布的文章数量
constructor(name: string, age: number) {
this.name = name;
this.age = age;
this.id = Math.random();
}
// 发布文章
publish(title: string) {
if (this._curNumber < this._publishNumber) {
console.log("发布一篇文章:" + title);
this._curNumber++;
} else {
console.log("你今日发布的文章数量已达到上限");
}
}
}
3)属性简写
- 如果某个属性是通过构造函数的参数传递的,并且不做任何处理的赋值给该属性
- 可以进行简写,语法糖
- 只需要在构造函数的参数前添加访问修饰符即可
class User {
readonly id: number;
constructor(
public name: string,
public age: number,
) {
this.id = Math.random();
}
}
3.访问器
- 用于控制属性的读取和赋值
- Vue 中的 computed 就是访问器
- 通过访问器访问的属性最好设置为私有属性
- 并且访问器内部的属性不能与访问器函数本身重名
- 否则会死循环,调用栈溢出
- 如果只声明了读取器,该属性自动变为只读属性
class User {
readonly id: number;
constructor(
public name: string,
private _age: number,
) {
this.id = Math.random();
}
/**
* 访问器
*/
// 设置器
// setAge(value: number) { // Java写法
set age(value: number) {
// C#写法
if (value < 0) {
this._age = 0;
} else if (value > 200) {
this._age = 200;
} else {
this._age = value;
}
}
// 读取器
// getAge() { // Java写法
get age() {
// C#写法
return Math.floor(this._age);
}
}
const u = new User("aa", 22);
u.age = 1.5;
console.log(u.age); // 1
4.案例实操:使用类优化扑克牌
- 用类改造程序
1)删除 utils,定义扑克牌类
// desck.ts
import { Mark, Color } from "./enums";
import { Card, Joker } from "./types";
// 一副扑克牌的类型
export class Deck {
private cards: Card[] = [];
// constructor() {
// this.init();
// }
constructor(cards?: Card[]) {
if (cards) {
this.cards = cards;
} else {
this.init();
}
}
// 创建扑克牌
private init() {
const marks = Object.values(Mark);
const colors = Object.values(Color);
for (const m of marks) {
for (const c of colors) {
this.cards.push({
color: c,
mark: m,
getString() {
return this.color + this.mark;
},
} as Card);
}
}
// 加入大小王
let joker: Joker = {
type: "small",
getString() {
return "jo";
},
};
this.cards.push(joker);
joker = {
type: "big",
getString() {
return "JO";
},
};
this.cards.push(joker);
}
// 打印扑克牌
print() {
let result = "\n";
this.cards.forEach((card, i) => {
result += card.getString() + "\t";
if ((i + 1) % 6 === 0) {
result += "\n";
}
});
console.log(result);
}
}
2)洗牌
// desck.ts
shuffle() {
for (let i = 0; i < this.cards.length; i++) {
const targetIndex = this.getRandom(0, this.cards.length);
const temp = this.cards[i];
this.cards[i] = this.cards[targetIndex];
this.cards[targetIndex] = temp;
}
}
// 洗牌辅助函数(无法取到最大值)
private getRandom(min: number, max: number) {
const dec = max - min;
return Math.floor(Math.random() * dec + min);
}
3)发牌
- 元组写法
// desck.ts
// // 结果有4个card[](元组),分别是三个玩家的牌和剩余的牌
// publish(): [Card[], Card[], Card[], Card[]] {
// const result:[Card[], Card[], Card[], Card[]] = [[],[],[],[]];
// }
// 1个card[]就是一个Deck
publish(): [Deck, Deck, Deck, Deck] {
let player1: Deck, player2: Deck, player3: Deck, left: Deck;
player1 = this.takeCards(17);
player2 = this.takeCards(17);
player3 = this.takeCards(17);
left = new Deck(this.cards);
return [player1, player2, player3, left];
}
// 发牌辅助函数(从剩余牌堆中摸牌)
private takeCards(n: number): Deck {
const cards: Card[] = [];
for (let i = 0; i < n; i++) {
cards.push(this.cards.shift() as Card);
}
return new Deck(cards);
}
- 对象写法
interface PublishResult {
player1: Deck;
player2: Deck;
player3: Deck;
left: Deck;
}
publish(): PublishResult {
let player1: Deck, player2: Deck, player3: Deck, left: Deck;
player1 = this.takeCards(17);
player2 = this.takeCards(17);
player3 = this.takeCards(17);
left = new Deck(this.cards);
return {
player1,
player2,
player3,
left,
};
}
4)主函数
import { Deck } from "./deck";
const deck = new Deck();
deck.shuffle();
console.log("======洗牌后======");
deck.print();
const res = deck.publish();
console.log("======发牌后======");
// console.log("======玩家1======");
// res[0].print();
// console.log("======玩家2======");
// res[1].print();
// console.log("======玩家3======");
// res[2].print();
// console.log("======剩余======");
// res[3].print();
console.log("===========玩家1========");
res.player1.print();
console.log("===========玩家2========");
res.player2.print();
console.log("===========玩家3========");
res.player3.print();
console.log("===========桌面========");
res.left.print();
(八)泛型
1.简介
1)问题引入
- 书写某个函数时,会丢失一些类型信息
- 比如参数、返回值、函数内部变量都需要同一个类型
- 但是函数调用前不能确定该类型
- 多个位置的类型应该保持一致或有关联的信息
function take(arr: any[], n: number): any[] {
if (n >= arr.length) return arr;
const newArr: any[] = []; // 类型推断为never,要显式标记any
for (let i = 0; i < n; i++) {
newArr.push(arr[i]);
}
return newArr;
}
const newArr1 = take([1, 2, 3, 4], 2);
const newArr2 = take(["a", "b", "c", "d"], 2);
2)引入泛型
- 指附属于函数、类、接口、类型别名之上的类型
- 相当于是一个类型变量
- 在定义时,无法预先知道具体的类型,可以用该变量来代替
- 只有到调用时,才能确定它的类型
- 很多时候,TS 会智能地根据传递的参数,推导出泛型的具体类型
- 前提是参数使用了泛型
- 如果无法完成推导,并且又没有传递具体的类型,默认为 空对象类型 never
- 泛型可以 设置默认值
function take<T = number>(arr: T[], n: number): T[] {}
- 泛型可以 解除某个功能和类型的耦合
2.在函数中使用泛型
- 在函数名之后写上
<泛型名称>
function take<T>(arr: T[], n: number): T[] {
if (n >= arr.length) return arr;
const newArr: T[] = [];
for (let i = 0; i < n; i++) {
newArr.push(arr[i]);
}
return newArr;
}
// const newArr1 = take<number>([1, 2, 3, 4], 2); // number[]
const newArr1 = take([1, 2, 3, 4], 2); // number[]
const newArr2 = take<string>(["a", "b", "c", "d"], 2); // string[]
3.在类型别名、接口、类中使用泛型
- 直接在名称后写上
<泛型名称>
1)类型别名
// 回调函数,判断数组中的某一项是否满足条件
type callback<T> = (n: T, i: number) => boolean;
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 arr1 = [1, 2, 3, 444];
console.log(filter(arr1, (n) => n % 2 !== 0));
2)接口
interface callback<T> {
(n: T, i: number): boolean;
}
3)类
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 + min);
}
}
4.泛型约束
- 用于现实泛型的取值
- 约束泛型应该有某些属性或方法
interface hasNameProperty {
name: string;
}
/**
* 将某个对象的name属性的每个单词的首字母大小,然后将该对象返回
*/
function nameToUpperCase<T extends hasNameProperty>(obj: T): T {
obj.name = obj.name
.split(" ")
.map((s) => s[0].toUpperCase() + s.substring(1))
.join(" ");
return obj;
}
const o = {
name: "kevin yuan",
age: 22,
gender: "男",
};
const newO = nameToUpperCase(o);
console.log(newO.name); // Kevin Yuan
5.多泛型
/**
* 将两个数组进行混合
* [1, 3, 4] + ["a", "b", "c"] = [1, "a", 3, "b", 4, "c"]
*/
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 < arr1.length; i++) {
result.push(arr1[i]);
result.push(arr2[i]);
}
return result;
}
const result = mixinArray([1, 3, 4], ["a", "b", "c"]);
result.forEach((r) => console.log(r)); // 1 a 3 b 4 c
6.案例实操:自定义字典类
- 定义一个字典类(Dictionary)
- 字典中会保存键值对的数据
1)键值对数据的特点
- 键(key)可以是任何类型,但不允许重复
- 值(value)可以是任何类型
- 每个键对应一个值
- 所有的键类型相同,所有的值类型相同
2)字典类中对键值对数据的操作
- 按照键,删除对应的键值对
- 循环每一个键值对
- 得到当前键值对的数量
- 判断某个键是否存在
- 重新设置某个键对应的值,如果不存在,则添加
3)代码实现:字典类
interface Callback<K, V> {
(k: K, v: V): void;
}
export class Dictionary<K, V> {
private keys: K[] = [];
private values: V[] = [];
/**
* 根据键,删除对应的键值对
* @param key 待删除的键
* @returns 是否删除成功
*/
delete(key: K): boolean {
const index = this.keys.indexOf(key);
if (index === -1) return false;
this.keys.splice(index, 1);
this.values.splice(index, 1);
return true;
}
/**
* 循环键值对作处理
* @param callback 遍历规则回调函数
*/
each(callback: Callback<K, V>): void {
this.keys.forEach((k, i) => {
callback(k, this.values[i]);
});
}
/**
* 获取当前键值对的数量
*/
get size() {
return this.keys.length;
}
/**
* 判断是否存在某个键
* @param key 待查找的键
*/
has(key: K): boolean {
return this.keys.indexOf(key) !== -1;
}
/**
* 设置键对应的值,不存在该键则添加
* @param key 待设置的键
* @param val 待设置的值
*/
set(key: K, val: V): void {
const index = this.keys.indexOf(key);
if (index === -1) {
this.keys.push(key);
this.values.push(val);
} else {
this.values[index] = val;
}
}
}
4)代码实现:主函数
import { Dictionary } from "./Dictionary";
const dictionary = new Dictionary<string, number>();
dictionary.set("a", 1);
dictionary.set("b", 2);
dictionary.set("c", 3);
dictionary.each((k, v) => console.log(`${k}: ${v}`));
console.log(`当前字典的键值对数量为 ${dictionary.size}`);
console.log("删除键b");
dictionary.delete("b");
dictionary.each((k, v) => console.log(`${k}: ${v}`));
console.log(`当前字典的键值对数量为 ${dictionary.size}`);
console.log(`当前字典是否含有键a:${dictionary.has("a")}`);
console.log(`当前字典是否含有键b:${dictionary.has("b")}`);
相关信息
实际上 TS 中的字典类就是 Map 结构
(九)项目实战:使用 React + TS 开发三字棋游戏
三字棋游戏,又叫井字棋游戏
1.项目准备
npx create-react-app 00reactgame --template typescript
1)tsconfig.json
{
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"downlevelIteration": true, // 支持迭代器
"allowJs": true, // 允许在ts文件中导入js,默认false
"skipLibCheck": true, // 跳过对声明文件的检查
"esModuleInterop": true, // 支持ES6标准默认导入commonjs,影响编译结果
"allowSyntheticDefaultImports": true, // 支持ES6标准默认导入commonjs,只影响语法
"strict": true, // 开启严格检查
"forceConsistentCasingInFileNames": true, // 区分大小写文件的引入
"module": "esnext", // 最新版本的模块化标准
"moduleResolution": "node", // 模块解析方式
"resolveJsonModule": true, // 允许解析json模块,导入后可以当对象使用
"isolatedModules": true, // 每个文件都视作模块,即至少得有一个export default
"noEmit": true, // 不生成编译的js文件,因为js文件还需要经过babel处理
"jsx": "preserve" // 解析jsx的方式
},
"include": ["src"]
}
2)dom.iterable
- 该库环境用于开启迭代器环境
- 只会影响开发时的语法
- 编译后遵循 target 配置项,编译为 ES5 语法
- 所以不会有兼容性问题
- ES6 的 Symble(符号)可以实现类的私有成员
- ES6 提出了迭代器的概念
- 表示数据可以被 for...of 循环,不是 for 循环
- 该数据不一定是数组
- 制作迭代器必须使用 Symble.Iterator
- 旧版本浏览器没有迭代器的概念
- 无法遍历 DOM 元素
- 需要开启配置
{
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"downlevelIteration": true // 支持迭代器
}
}
const doms = document.getElementsByTagName("button");
for (const dom of doms) {
}
3)"module": "esnext"
- 使用 ESNext 最新版本的模块化标准
- 无需担心兼容性问题
- 旧版本要求 import 语句必须顶层声明
- 编译流程:webpack -> ts -> js -> babel -> 最终结果
- 在 ts 编译为 js 时都使用的新版本语法,但经过 babel 编译后会使用旧版本语法
- 最后交回 webpack 打包
2.在 React 中使用 TS
1)使用 JS 开发时的问题
- 难以确定某个组件有哪些属性需要传递
- 查看官方文档
- 难以确定每个属性应该传递什么类型
- 传递事件时,有哪些参数
- 错误发生在运行时
- 可以通过 propTypes 约束属性的类型
- 但是发生错误的时间点是在运行时
2)结合 TS 解决
- TS 是一套静态的类型系统
3)React 组件分类
- 展示组件
- 通常是函数式组件
- 只做页面展示
- 容器组件
- 通常是类组件
- 只做数据处理
4)函数组件两种约束方式
import React from "react";
interface IProps {
num: number;
onChange?: (n: number) => void;
}
// 写法一
export function CountComp(props: IProps) {
return (
<div>
<button
onClick={() => {
if (props.onChange) {
props.onChange(props.num - 1);
}
}}
>
-
</button>
<span>{props.num}</span>
<button
onClick={() => {
if (props.onChange) {
props.onChange(props.num + 1);
}
}}
>
+
</button>
</div>
);
}
// 写法二
export const CountComp: React.FC<IProps> = (props) => {
return (
<div>
<button
onClick={() => {
if (props.onChange) {
props.onChange(props.num - 1);
}
}}
>
-
</button>
<span>{props.num}</span>
<button
onClick={() => {
if (props.onChange) {
props.onChange(props.num + 1);
}
}}
>
+
</button>
</div>
);
};
- 写法二由于使用 React.FC 约束
- 比写法一多了一个属性 props.children
- 如果需要为接口的 可选参数 设置默认值,必须使用写法二
interface IProps {
chesses: ChessType[];
onClick?: (index: number) => void;
isGameOver?: boolean;
}
export const BoardComp: React.FC<IProps> = ({ chesses, onClick, isGameOver }) => {
return <div className="board">{list}</div>;
};
BoardComp.defaultProps = {
isGameOver: false,
};
5)类组件的约束方式
警告
- 如果要关联 state 和 setState 的约束
- 必须在 React.Component 后声明泛型
interface IState {
msg: string;
desc: string;
}
export class CountComp extends React.Component<IProps, IState> {
// 正常初始化会覆盖IState,需要显式约束state
// state = {
state: IState = {
msg: "",
desc: "",
// test: 2
};
render() {
// this.state.msg
return (
<div>
<button
onClick={() => {
if (this.props.onChange) {
this.props.onChange(this.props.num - 1);
}
}}
>
-
</button>
<span>{this.props.num}</span>
<button
onClick={() => {
if (this.props.onChange) {
this.props.onChange(this.props.num + 1);
}
}}
>
+
</button>
</div>
);
}
}
2.制作棋子组件
1)components/ChessComp.tsx
import React from "react";
import { ChessType } from "../types/enums";
import "./ChessComp.css";
interface IProps {
type: ChessType;
onClick?: () => void;
}
export function ChessComp({ type, onClick }: IProps) {
let chess = null;
if (type === ChessType.red) {
chess = <div className="chess-item red"></div>;
} else if (type === ChessType.black) {
chess = <div className="chess-item black"></div>;
}
return (
<div
className="chess"
onClick={() => {
if (type === ChessType.none && onClick) {
onClick();
}
}}
>
{chess}
</div>
);
}
2)App.tsx
import React from "react";
import { ChessComp } from "./components/ChessComp";
import { ChessType } from "./types/enums";
export class App extends React.Component {
render() {
return (
<div>
<ChessComp type={ChessType.none} onClick={() => console.log("被点击了")} />
<ChessComp type={ChessType.red} onClick={() => console.log("被点击了")} />
<ChessComp type={ChessType.black} onClick={() => console.log("被点击了")} />
</div>
);
}
}
3.制作棋盘组件
1)components/BoardComp.tsx
import React from "react";
import { ChessType } from "../types/enums";
import { ChessComp } from "./ChessComp";
import "./BoardComp.css";
interface IProps {
chesses: ChessType[];
onClick?: (index: number) => void;
isGameOver?: boolean;
}
export const BoardComp: React.FC<IProps> = ({ chesses, onClick, isGameOver }) => {
const list = chesses.map((type, i) => (
<ChessComp
key={i}
type={type}
onClick={() => {
onClick && !isGameOver && onClick(i);
}}
/>
));
return <div className="board">{list}</div>;
};
BoardComp.defaultProps = {
isGameOver: false,
};
2)App.tsx
import React from "react";
import { BoardComp } from "./components/BoardComp";
import { ChessType } from "./types/enums";
const types: ChessType[] = [
ChessType.none,
ChessType.red,
ChessType.black,
ChessType.none,
ChessType.red,
ChessType.black,
ChessType.none,
ChessType.red,
ChessType.black,
];
export class App extends React.Component {
render() {
return (
<div>
<BoardComp chesses={types} onClick={(i) => console.log(i)} isGameOver={true} />
</div>
);
}
}
3)非空断言
- 在数据后加上
!
- 告诉 TS 不用考虑该数据为空的情况
export const BoardComp: React.FC<IProps> = ({ chesses, onClick, isGameOver }) => {
/**
* 设置了默认值的可选参数会判断为boolean | undefined
* 实际上不可能为undefined,正常没有影响,涉及到计算时会有影响
* 可以使用类型断言强制约束为boolean
* 非空断言
*/
// const isOver = isGameOver as boolean;
const isOver = isGameOver!;
const list = chesses.map((type, i) => (
<ChessComp
key={i}
type={type}
onClick={() => {
onClick && !isOver && onClick(i);
}}
/>
));
return <div className="board">{list}</div>;
};
4.制作游戏组件
- 提供并维护游戏中的数据
- 有状态组件
1)App.tsx
import React from "react";
import { GameComp } from "./components/GameComp";
export class App extends React.Component {
render() {
return (
<div>
<GameComp />
</div>
);
}
}
2)components/GameStatusComp.tsx
import React from "react";
import { ChessType, GameStatus } from "../types/enums";
import "./GameStatusComp.css";
interface IProps {
status: GameStatus;
next: ChessType.red | ChessType.black;
}
export function GameStatusComp(props: IProps) {
let content: JSX.Element;
if (props.status === GameStatus.gaming) {
if (props.next === ChessType.red) {
content = <div className="red">红方落子</div>;
} else {
content = <div className="black">黑方落子</div>;
}
} else {
if (props.status === GameStatus.redWin) {
content = <div className="win red">红方胜利</div>;
} else if (props.status === GameStatus.blackWin) {
content = <div className="win black">黑方胜利</div>;
} else {
content = <div className="win equal">平局</div>;
}
}
return <div className="status">{content}</div>;
}
3)components/GameComp.tsx
import React from "react";
import { ChessType, GameStatus, NextChess } from "../types/enums";
import { BoardComp } from "./BoardComp";
import { GameStatusComp } from "./GameStatusComp";
interface IState {
chesses: ChessType[];
gameStatus: GameStatus;
nextChess: ChessType.red | ChessType.black;
// nextChess: NextChess;
}
export class GameComp extends React.Component<{}, IState> {
state: IState = {
chesses: [],
gameStatus: GameStatus.gaming,
nextChess: ChessType.black,
};
/**
* 初始化数据
*/
init() {
const arr: ChessType[] = [];
for (let i = 0; i < 9; i++) {
arr.push(ChessType.none);
}
this.setState({
chesses: arr,
gameStatus: GameStatus.gaming,
nextChess: ChessType.black,
});
}
componentDidMount() {
this.init();
}
/**
* 处理棋子的点击事件
* 执行时说明游戏没有结束且点击的位置没有棋子
* @param index 棋子下标
*/
handleChessClick(index: number) {
const chesses: ChessType[] = [...this.state.chesses];
chesses[index] = this.state.nextChess;
this.setState((prevState) => ({
chesses,
nextChess: prevState.nextChess === ChessType.red ? ChessType.black : ChessType.red,
gameStatus: this.getGameStatus(chesses, index),
}));
}
/**
* 获取当前游戏状态
* @param chesses 点击后最新的棋子数组
* @param index 最新落子的位置
*/
getGameStatus(chesses: ChessType[], index: number): GameStatus {
// 1.判断是否有一方获得胜利
const horMin = Math.floor(index / 3) * 3;
const verMin = Math.floor(index % 3);
if (
// 横向三连
(chesses[horMin] === chesses[horMin + 1] && chesses[horMin] === chesses[horMin + 2]) ||
// 纵向三连
(chesses[verMin] === chesses[verMin + 3] && chesses[verMin] === chesses[verMin + 6]) ||
// 斜向三连
(chesses[0] === chesses[4] && chesses[0] === chesses[8] && chesses[0] !== ChessType.none) ||
(chesses[2] === chesses[4] && chesses[2] === chesses[6] && chesses[2] !== ChessType.none)
)
return chesses[index] === ChessType.red ? GameStatus.redWin : GameStatus.blackWin;
// 2.判断是否平局
if (!chesses.includes(ChessType.none)) return GameStatus.equal;
// 3.游戏正在进行
return GameStatus.gaming;
}
render() {
return (
// 严格来说,容器组件不能有样式
<div
style={{
textAlign: "center",
}}
>
<h1>三连棋游戏</h1>
<GameStatusComp status={this.state.gameStatus} next={this.state.nextChess} />
<BoardComp chesses={this.state.chesses} isGameOver={this.state.gameStatus !== GameStatus.gaming} onClick={this.handleChessClick.bind(this)} />
<button
onClick={() => {
this.init();
}}
>
重新开始
</button>
</div>
);
}
}
4)types/enums.ts
export enum ChessType {
none,
red,
black,
}
export enum NextChess {
red = ChessType.red,
black = ChessType.black,
}
export enum GameStatus {
/**
* 正在游戏中
*/
gaming,
/**
* 红方胜利
*/
redWin,
/**
* 黑方胜利
*/
blackWin,
/**
* 平局
*/
equal,
}