4-1. 字面量类型的问题
前言
任务列表
- 理解使用字面量类型来约束变量的取值范围所导致的一些问题
- 重复代码问题 - 可以使用 type 类型别名来解决
- 逻辑含义和真实值混淆的问题 - 没法解决
- 编译结果中不含字面量类型约束信息,在编译结果中无法获取到取值范围中可能都有哪些值 - 没法解决
认识某些场景下,若我们要限定变量的取值范围时,使用先前所学习的知识点来实现所导致的一系列问题。
notes
扩展类型
ts 给我们提供的基本类型不足以满足我们的实际需求,我们需要基于 ts 给我们提供的基本类型进行扩展。
扩展类型就是我们自己定义的一些类型:
- 类型别名
- 枚举
- 接口
- 类
:::tips
枚举将在本章进行介绍;
类型别名在 3. 基本类型检查 介绍过;
接口和类也都将在后续课程中逐一介绍到;
:::
我们本章节要学习的“枚举”,通常用于约束某个变量的取值范围。
虽然,我们前面所学习的“字面量”和“联合类型”配合使用,也可以达到同样的效果。但是,它们还存在一些问题,这些问题就是本节课要介绍的点。
字面量类型的问题
- 在类型约束位置,会产生重复代码。
- 逻辑含义和真实的值产生了混淆,会导致当修改真实值的时候,产生大量的修改。
- 字面量类型不会进入到编译结果。
对于有关重复代码的问题,可以使用类型别名来解决。但是,另外两个问题,使用类型别名就没法解决了。
上面提到的这些问题,都将在 codes 部分,以代码的形式来呈现。问题具体如何解决,会在 3-2 中介绍。
codes
重复代码的问题
let gender: "male" | "female";
gender = "male";
function searchUsers(g: "male" | "female") {}
使用字面量类型,会在类型约束的位置产生大量的重复代码。对于该问题,我们可以使用之前学习到的类型别名来解决。
type Gender = "male" | "female";
let gender: Gender;
gender = "male";
function searchUsers(g: Gender) {}
使用类型别名解决在类型约束位置产生的重复代码问题。
逻辑含义和真实值的问题
我们知道,对于性别,无非就是“男 | 女”。但是,我们显示给用户看的真实的值,可就不一定是“男 | 女”了,也可以是“先生 | 女士”、“帅哥 | 美女”、“boy | girl”、“male | female”等等。
那么就会有这样一种情况,倘若有一天,我们需要修改展示给用户看的值,这样我们需要修改的地方就会很多,工作量就会很大,而且容易出错。而我们希望的是,只要修改类型约束部分,修改一个地方就可以完成所有的修改。
对于这样的需求,使用原来的字面量式写法,显然就无法解决了。但是若使用枚举,就可以很好地解决这样的问题。
type Gender = "男" | "女";
let gender: Gender;
gender = "男";
function searchUsers(g: Gender) {}
当我们修改完 1 后,还得修改 2。
虽然在个案例中看不出有什么大问题,但是试想一下,若程序的规模很大,很多地方都用到了 Gender 类型,那我们需要修改的量,可就不是上边这么一丢丢了。
编译结果
let gender;
gender = "男";
function searchUsers(g) { }
从编译结果中,我们可以看出:类型别名是不会生成到最终的结果中的。
若我们在编辑结果中想要获取到 gender 所有可能的值,也就是 Gender 中限定的值,那么使用上面这种类型别名的写法,也是无法获取到的。(该需求也可以使用「枚举」来解决)
4-2. 枚举的使用
前言
任务列表
- 掌握枚举的语法
- 理解枚举的编译结果
- 理解数字枚举的特点
- 掌握枚举的最佳实践
notes
只要是限定变量的取值范围,我们都可以使用枚举来实现。
本节学习约束某个变量的取值范围的最佳实践 - 枚举。
enum 枚举名 {
枚举字段1 = 值1,
枚举字段2 = 值2,
...
}
- 枚举会出现在编译结果中,编译结果中表现为一个对象;
- 枚举的字段值可以是字符串或数字;
- 数字枚举的值会自动自增;
- 被数字枚举约束的变量,可以直接赋值为数字;
- 数字枚举的编译结果(双向映射 k <-> v)和字符串枚举的编译结果(单向映射 k -> v)有差异;
:::tips 至于为什么数字枚举允许我们直接赋值为数字,想要了解的话,可以看看本章的最后一节4-4. 扩展知识:枚举的位运算 :::
最佳实践
- 尽量不要在一个枚举中,即出现字符串字段,又出现数字字段;
- 使用枚举式,尽量使用枚举字段的名称,而不使用真实的值;
codes
字符串枚举
enum Gender { // 枚举的写法
male = "男",
female = "女",
}
let gender: Gender;
gender = Gender.male;
function searchUsers(g: Gender) {}
male 和 female 是逻辑名称,它们映射的值,是真实展示给用户看的值。
我们在写的时候,写的是逻辑名称,比如:Gender.male
但是读取到的,是真实的值。
认识一下枚举的图标:
一键重命名:
我们通常遇到的需求,修改的都是展示给用户看的值,一般不会有需求是改变逻辑含义的。
但是,若我们需要修改逻辑名,也是可以一键修改的,在指定的逻辑名上右键,选择重命名符号即可修改。(或直接按 F2)
编译结果分析:
在命令行输入 tsc
命令对 1.ts 进行编译,生成的编译结果文件如下:
var Gender;
(function (Gender) {
Gender["Male"] = "\u7537";
Gender["female"] = "\u5973";
})(Gender || (Gender = {}));
let gender;
gender = Gender.Male;
function searchUsers(g) { }
编码
由于我们使用到了中文,会进行 Unicode 编码:
- “男” 对应的 Unicode 编码是 \u7537
- “女” 对应的 Unicode 编码是 \u5973
在编译结果中,枚举会被识别为一个对象。
// 该表达式是一个函数
(function (Gender) {
Gender["Male"] = "\u7537";
Gender["female"] = "\u5973";
})
(function (Gender) {
Gender["Male"] = "\u7537";
Gender["female"] = "\u5973";
})(Gender || (Gender = {}));
这么写相当于调用上一步创建的函数,并传入参数:Gender || (Gender = {})
参数分析:
该参数也是一个表达式,该表达式的返回值得看 Gender 的值,若 Gender 的值是空,那么会执行 Gender = {}
表达式,该赋值表达式执行后,Gender 被赋值为一个空对象 {}
。由于赋值表达式的返回值就是所赋的值,所以该表达式会返回该对象。
var Gender {
Male: "男",
female: "女"
}
从编译结果中获取约束条件范围内的所有可能的取值:
enum Gender {
Male = "男",
female = "女",
}
function printGenders() {
const vals = Object.values(Gender);
vals.forEach((v) => console.log(v));
}
printGenders();
在编译结果中,我们也可以获取到 Gender 中定义的真实值。
数字枚举
枚举的字段值可以是字符串或数字,如果字段值是数字的话,那么就是数字枚举。
enum Level {
level1 = 1,
level2 = 2,
level3 = 3,
}
let l: Level = Level.level1;
l = Level.level2;
对比数字枚举和字符串枚举的编译结果差异:
var Level;
(function (Level) {
Level[Level["level1"] = 1] = "level1";
Level[Level["level2"] = 2] = "level2";
Level[Level["level3"] = 3] = "level3";
})(Level || (Level = {}));
let l = Level.level1;
l = Level.level2;
数字枚举的编译结果和字符串枚举是有差异的,可以理解为:
- 字符串枚举:单向映射
key -> value
- 数字枚举:双向映射
key <-> value
(function (Level) {
Level[Level["level1"] = 1] = "level1";
Level[Level["level2"] = 2] = "level2";
Level[Level["level3"] = 3] = "level3";
})(Level || (Level = {}));
// 相当于生成了下面这样一个对象
{
level1: 1,
level2: 2,
level3: 3,
"1": "level1",
"2": "level2",
"3": "level3",
}
数字枚举默认自增:
enum Level {
level1 = 1,
level2, // 相当于写了 level2 = 2,
level3, // 相当于写了 level3 = 3,
}
let l: Level = Level.level1;
l = Level.level2;
数字枚举的值会自动自增;
这种 4.ts 中的代码,和 3.ts 中的代码,是完全等效的。
枚举字段值都为空:
若枚举字段的值,都没有填写,那么默认是数字枚举,并且起始字段的值为 0。
enum Level {
level1,
level2,
level3,
}
// 等效写法
enum Level {
level1 = 0,
level2 = 1,
level3 = 2,
}
被数字枚举约束的变量,我们可以直接将真实值赋给它(但是不推荐这么做)
enum Level {
level1,
level2,
level3,
}
let lev: Level = 1; // 不会报错
lev = 2; // 不会报错
被数字枚举约束的变量,可以直接赋值为数字。但是,不推荐这么写,这么写意味着我们又在直接使用真实值了。
若要按照上述写法来写,那么用之前学习的内容就完全可以实现了,这让枚举失去了意义。
type Level = 0 | 1 | 2;
let lev: Level = 1;
lev = 2;
4-3. 练习:使用枚举优化扑克牌程序
前言
优化步骤:
袁进代码
type Color = "♥" | "♠" | "♦" | "♣"; // 扑克花色
type NormalCard = {
// 一张牌
color: Color;
mark: number;
};
type Deck = NormalCard[]; // 一副牌
function createDeck(): Deck {
const deck: Deck = [];
for (let i = 1; i <= 13; i++) {
deck.push(
{
mark: i,
color: "♠",
},
{
mark: i,
color: "♣",
},
{
mark: i,
color: "♥",
},
{
mark: i,
color: "♦",
}
);
}
return deck;
}
function printDeck(deck: Deck) {
let result = "";
deck.sort(() => Math.random() - 0.5); // 乱序
deck.forEach((pocker, i) => {
const num = pocker.mark;
if (num <= 10) {
result += num + pocker.color + " ";
} else if (num === 11) {
result += "J" + pocker.color + " ";
} else if (num === 12) {
result += "Q" + pocker.color + " ";
} else if (num === 13) {
result += "K" + pocker.color + " ";
}
// 每打印 17 张牌,换行。
if ((i + 1) % 17 === 0) {
result += "\n";
}
});
console.log(result);
}
const deck = createDeck();
printDeck(deck);
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",
jack = "J",
queen = "Q",
king = "K",
}
type NormalCard = {
// 一张牌
color: Color;
mark: Mark;
};
type Deck = NormalCard[]; // 一副牌
function createDeck(): Deck {
const deck: Deck = [];
const marks = Object.keys(Mark);
const colors = Object.keys(Color);
for (const m of marks) {
for (const c of colors) {
deck.push({
mark: Mark[m],
color: Color[c],
});
}
}
return deck;
}
function printDeck(deck: Deck) {
let result = "";
deck.sort(() => Math.random() - 0.5); // 乱序
deck.forEach((pocker, i) => {
result += pocker.color + pocker.mark + " ";
if ((i + 1) % 17 === 0) {
result += "\n";
}
});
console.log(result);
}
const deck = createDeck();
printDeck(deck);
优化前后的对比:主要优化了一副扑克牌的创建流程
对于枚举,我们直接将其视作一个对象来处理即可。从枚举的编译结果中可以得知枚举的编译结果就是对象。
我的代码
type Colors = "❤" | "♠" | "♣" | "◇";
type Poker = {
// 一张牌
number: number | string;
color?: Colors;
};
type Deck = Poker[]; // 一副牌
// 创建一副牌
function createDeck() {
const deck: Poker[] = [];
// 插入 A ~ K
for (let i = 1; i <= 13; i++) {
let number: number | string;
if (i === 1) number = "A";
if (i === 11) number = "J";
if (i === 12) number = "Q";
if (i === 13) number = "K";
deck.push({
number: i,
color: "❤",
});
deck.push({
number: i,
color: "♠",
});
deck.push({
number: i,
color: "♣",
});
deck.push({
number: i,
color: "◇",
});
}
// 插入大小王
deck.push({
number: "joker",
});
// 插入大王
deck.push({
number: "JOKER",
});
deck.sort(() => Math.random() - 0.5); // 打乱牌序
return deck;
}
// 打印一副扑克
function printDeck(deck: Deck) {
let result = "底牌:";
const desk = deck.splice(0, 3); // 底牌
desk.forEach((poker, i) => {
result += poker.color ? ` ${poker.color}${poker.number}` : ` ${poker.number}`;
});
result += '\n用户1:'
deck.forEach((poker, i) => {
if(i % 17 === 0 && i !== 0) result += `\n用户${i / 17 + 1}:`;
result += poker.color ? ` ${poker.color}${poker.number}` : ` ${poker.number}`;
});
console.log(result);
}
const deck = createDeck();
printDeck(deck);
// 黑桃(Spade)、红桃(Heart)、方块(Diamond)、梅花(Club)
enum Colors { // 花色
Heart = "❤",
Spade = "♠",
Club = "♣",
Diamond = "◇",
}
// one,two,three,four,five,six,seven,eight,nine,ten,eleven,twelve,thirteen
// 牌编号
enum Nums {
one = "A",
two = "2",
three = "3",
four = "4",
five = "5",
six = "6",
seven = "7",
eight = "8",
nine = "9",
ten = "10",
eleven = "J",
twelve = "Q",
thirteen = "K",
}
type Poker = {
// 一张牌
number: Nums | "JOKER" | "joker";
color?: Colors;
};
type Deck = Poker[]; // 一副牌
// 创建一副牌
function createDeck() {
const deck: Deck = [];
// 插入大小王
deck.push({
number: "joker",
});
// 插入大王
deck.push({
number: "JOKER",
});
// 插入 A ~ K
const colorKeys = Object.keys(Colors);
const numKeys = Object.keys(Nums);
for (let i = 0; i < colorKeys.length; i++) {
for (let j = 0; j < numKeys.length; j++) {
deck.push({
number: Nums[numKeys[j]],
color: Colors[colorKeys[i]]
});
}
}
deck.sort(() => Math.random() - 0.5); // 打乱牌序
return deck;
}
// 打印一副扑克
function printDeck(deck: Deck) {
let result = "底牌:";
const desk = deck.splice(0, 3); // 底牌
desk.forEach((poker, i) => {
result += poker.color ? ` ${poker.color}${poker.number}` : ` ${poker.number}`;
});
result += '\n用户1:'
deck.forEach((poker, i) => {
if(i % 17 === 0 && i !== 0) result += `\n用户${i / 17 + 1}:`;
result += poker.color ? ` ${poker.color}${poker.number}` : ` ${poker.number}`;
});
console.log(result);
}
const deck = createDeck();
printDeck(deck);
export {};
4-4. 扩展知识:枚举的位运算
前言
扩展内容暂且不看。。。