7-1. 概述

面向对象的开发思想,需要慢慢培养,本章介绍的是「类」,类和面向对象是息息相关的。

但是,并非说使用到了类就是使用了面向对象的开发方式,这完全是两码事。面向对象的思维方式,不是一朝一夕可以养成的。后续其它几节的内容,主要介绍的重点将放在:ts 相较于 js,对于「类」的写法,都新增了哪些语法,进行了哪些扩展。而不会过多的介绍「面向对象」这种开发思想。

说简单些,就是本章节的重心将放在:在 ts 中写类和在 js 中写类,都有哪些不同的地方;ts 在 js 的基础上,都进行了哪些扩展。

:::tips 袁老提到,他使用面向对象的开发方式来开发,达到小有成就的程度,花费了将就 3 年的时间。 :::

7-2. 新增的类语法

前言

任务列表

  • strictPropertyInitialization开启更加严格的属性初始化
  • 属性列表
    • 属性默认值
    • 可选属性
    • 只读属性
  • 访问修饰符
    • public
    • private
  • 属性简写

notes

定义类的错误写法

错误写法:

  1. class User {
  2. constructor (name: string, age: number) {
  3. this.name = name; // ×
  4. this.age = age; // ×
  5. }
  6. }

在 js 中,上面这种写法是完全 OK 的
在 ts 中,上面这种写法会报错

image.png

定义对象的错误写法

const user = {};
user.name = "dahuyou"; // ×
user.age = 23; // ×

上述写法在 ts 中,也是不被允许的。

image.png

🤔 上面这些写法在 js 中显得很正常,为什么在 ts 中就报错了呢?
TS 认为:对象身上的属性是固定的,一开始就得提前声明好,不能后续随意动态增加,所以上述写法都是不允许的。

属性列表

使用属性列表来描述类中的属性,提前声明好 class 中都有哪些属性:

class User {
  name: string
  age: number
  constructor (name: string, age: number) {
    this.name = name;
    this.age = age;
  }
}

image.pngimage.png

属性列表不会出现在编译结果中

class User {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
}

属性初始化

虽然属性列表中罗列出该 class 应该具备的属性,但是我们在初始化 class 属性的时候,不一定会完全按照属性列表来。

class User {
  name: string
  age: number
  constructor (name: string, age: number) {
    this.name = name;
    // this.age = age;
  }
}

上面这种写法,在默认情况下是被允许的。

假设这是我们不希望看到的,明明 class 的属性列表中声明了俩属性 nameage,但是我们仅对 name 属性进行了初始化操作。

我们希望看到的效果:如果出现上述这样情况 —— 在属性初始化时有属性没有初始化,那么抛出错误。如果想要实现这种需求,那么我们可以通过配置 strictPropertyInitialization 来实现更加严格的类属性初始化检查。

strictPropertyInitialization

添加 strictPropertyInitialization配置,实现更加严格的类属性初始化检查。

官方描述:Check for class properties that are declared but not set in the constructor.
译:检查类中那些被声明了的,但是没有在 constructor 中初始化的属性。

默认值:false

{
  "compilerOptions": {
    "target": "es2016",
    "module": "commonjs",
    "lib": [
      "es2016"
    ],
    "outDir": "./dist",
    "strictNullChecks": true,
    "strictPropertyInitialization": true,
    "removeComments": true
  },
  "include": [
    "./src"
  ],
}

image.pngimage.png

class User {
  name: string
  age: number
  constructor (name: string, age: number) {
    this.name = name;
    this.age = age;
  }
}

属性默认值

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("dahuyou", 23); // ×

image.png

现在我们想要设置 gender 属性的默认值为”男”,当没有给 gender 提供值的时候,就使用默认值。

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("dahuyou", 23);
class User {
  constructor(name, age, gender = "男") {
    this.name = name;
    this.age = age;
    this.gender = gender;
  }
}
const u = new User("dahuyou", 23);
class User {
  name: string
  age: number
  gender: "男" | "女" = "男"
  constructor (name: string, age: number) {
    this.name = name;
    this.age = age;
  }
}

const u = new User("dahuyou", 23);
class User {
  constructor(name, age) {
    this.gender = "男"; // 在构造器最前边插入
    this.name = name;
    this.age = age;
  }
}
const u = new User("dahuyou", 23);

🤔 若两种写法同时存在,并且存在冲突,听谁的?
仔细观察编译结果,不难发现写法1会覆盖写法2,最终会听写法1的。

:::tips image.png

这种细节,简单了解一下就好,现在回看之前的笔记,才发现原来自己当时记录的这么细。

难怪学习进度这么慢。。。 :::

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("dahuyou", 23);
console.log(u.gender); // => "男"
class User {
  constructor(name, age, gender = "男") {
    this.gender = "女";
    this.name = name;
    this.age = age;
    this.gender = gender;
  }
}
const u = new User("dahuyou", 23);
console.log(u.gender);

可选属性

可选属性多种写法,最常见的是?:
其它写法:
| undefined
| null = null

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

const u = new User("dahuyou", 23);
console.log(u.pid); // => undefined
class User {
  constructor(name, age) {
    this.gender = "男";
    this.name = name;
    this.age = age;
  }
}
const u = new User("dahuyou", 23);
console.log(u.pid);

?:这种写法,编译结果中会直接忽略掉可选属性。

image.png

?:这种写法,其实就等效于string | undefined这种写法。

class User {
  name: string
  age: number
  gender: "男" | "女" = "男"
  pid: string | undefined
  constructor (name: string, age: number) {
    this.name = name;
    this.age = age;
  }
}

const u = new User("dahuyou", 23);
console.log(u.pid); // => undefined
class User {
  constructor(name, age) {
    this.gender = "男";
    this.name = name;
    this.age = age;
  }
}
const u = new User("dahuyou", 23);
console.log(u.pid);
class User {
  name: string
  age: number
  gender: "男" | "女" = "男"
  pid: string | null = null
  constructor (name: string, age: number) {
    this.name = name;
    this.age = age;
  }
}

const u = new User("dahuyou", 23);
console.log(u.pid); // => null
class User {
  constructor(name, age) {
    this.gender = "男";
    this.pid = null;
    this.name = name;
    this.age = age;
  }
}
const u = new User("dahuyou", 23);
console.log(u.pid);

| null = null这其实相当于给属性 pid 添加了一个默认值 null

  • 第一个 null 相当于一个字面量约束
  • 第二个 null 是一个具体的值 null

通过属性默认值的方式来实现可选属性,这么做合情合理
具备默认值的属性,必然是可选的

只读属性

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

const u = new User("dahuyou", 23);
u.id = 123; // ×

image.png

访问修饰符

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

访问修饰符 描述
public 公开的
默认的访问修饰符,所有位置均可访问
private 私有的
只有在类中可以访问
protected 受保护的
只能在自身和子类中访问
(暂不介绍,会在进阶部分 「1-2. 类的继承」 中介绍)

:::tips 开发习惯 - 私有属性命名

当我们标识某个属性是一个私有属性时,一般在命名时,会加上下划线,比如:_publishNumber_curNumber :::

class User {
  readonly id: number
  name: string
  age: number
  gender: "男" | "女" = "男"
  pid: string | undefined
  private publishNumber: number = 3 // 每天一共可以发布多少篇文章
  private curNumber: number = 0 // 当前已经发布的文章数量
  constructor (name: string, age: number) {
    this.id = Math.random();
    this.name = name;
    this.age = age;
  }
}

const u = new User("dahuyou", 23);
u.publishNumber // ×

image.png

image.png

前面所写的属性 id、name、age、gender、pid,虽然没有明写修饰符,它们其实也都有访问修饰符,那就是默认的 public。

表示这些属性可以在任意位置访问。加上 private 修饰符的属性,表示类内部的私有属性,只能在类内部访问。

:::tips 记录一个小细节

🤔 为啥 public 修饰的成员它们始终都是高亮显示?而被 private 访问修饰符修饰的成员,如果在 User 类中没有访问,它们就是偏暗的呢?

因为被 public 修饰的成员,可以在类外部被访问,对于外部的一些操作,编辑器没法全部都检测到(成本太高),自然就没法给我们提供提示,默认就高亮显示啦。(猜想是这个原因)

而被 private 修饰的成员,它的上下文仅限于类中,编辑器能够轻易地检测出该成员是否有被访问。 :::

class User {
  readonly id: number;
  name: string;
  age: number;
  gender: "男" | "女" = "男";
  pid: string | undefined;
  private publishNumber: number = 3; // 每天一共可以发布多少篇文章
  private curNumber: number = 0; // 当前已经发布的文章数量
  constructor(name: string, age: number) {
    this.id = Math.random();
    this.name = name;
    this.age = age;
  }

  publish(title: string) {
    if (this.curNumber < this.publishNumber) {
      console.log(`${title}已发布`);
      this.curNumber++;
    } else {
      console.log("发布已达上线");
    }
  }
}

const u = new User("dahuyou", 23);

image.png

当我们在类内部访问了私有属性,会发现编辑器 vscode 会令它们高亮显示,依次来提示我们它们有被访问。

u.publish('title-1')
u.publish('title-2')
u.publish('title-3')
u.publish('title-4')
u.publish('title-5')

image.png

访问修饰符起到的重要作用

这里简单介绍一个模拟的场景,通过该场景来了解访问修饰符的作用。

可能我们写的这个 User 类,会被其它同事调用,他们不清楚 User 类中的哪些成员可以修改哪些不能修改。他们调用若干次 publish 后,发现无法再继续调用了,但是为了实现他们手头正在写的某个需求,需要让用户能够继续发布文章,可能就会尝试去修改用户发布文章的上限publishNumber、文章当前的发布数量curNumber。从而写出类似于下面这些代码:

u.publishNumber = xxx;
u.curNumber = xxx;

如果我们没有使用 private 修饰符对 publishNumber、curNumber 进行约束的话,那么他们就可以修改成功。随后开开心心地提交代码,完成需求,发版上线。这种错误操作一旦在我们的工程中大量出现,那么后期维护起来,成本是不可小觑的。

对于这些不应该被允许的做法,我们希望当这种情况发生时,就立刻抛出错误。

同时,尽可能少暴露一些东西,这也是我们开发的一个原则。现实生活中,很多产品也是这么做的,尽可能的精简,比如电视机遥控器的按钮,从一开始的一堆按钮,到现在的少数按钮。因为只要提供这些少数按钮,就可以轻易地实现我们所需的需求,自然就没必要暴露那么多不必要的东西了。

这就是使用修饰符来约束类属性的作用。

symbol

在 js 中也可以通过 symbol 实现私有属性,ts 是 js 的超集,自然也是可以使用 symbol 来实现的。

与访问修饰符 private 比较起来,symbol 的写法会相对麻烦一些。

并且在 ts 中使用访问修饰符 private 的另一个好处在于:当我们错误地尝试访问私有属性时,编辑器能够提供良好的错误提示。

综上,在 ts 中推荐采用 private 访问修饰符的方式来设置私有属性,而不是使用 symbol

属性简写

👇🏻 要介绍的是一个 ts 给我们提供的语法糖:

很多属性,都是通过参数传递进来,然后直接 this.xxx = xxx 赋值,除了这个赋值操作外,就没有其它额外操作了。

对于这样的场景,ts 为我们提供了更加便利的语法糖:访问修饰符 + 参数

image.png

简写之前的常规做法:

  1. 提前声明属性 name: string
  2. 定义好参数 name: string
  3. 使用传入的参数给提前声明的属性赋值 this.name = name

不难想象,在一个 class 中,这样的代码会经常出现,然而这些代码,在 ts 中其实是可以简写的。

class User {
  readonly id: number;
  gender: "男" | "女" = "男";
  pid: string | undefined;
  private publishNumber: number = 3; // 每天一共可以发布多少篇文章
  private curNumber: number = 0; // 当前已经发布的文章数量
  constructor(public name: string, public age: number) {
    this.id = Math.random();
  }

  publish(title: string) {
    if (this.curNumber < this.publishNumber) {
      console.log(`${title}已发布`);
      this.curNumber++;
    } else {
      console.log("发布已达上线");
    }
  }
}

const u = new User("dahuyou", 23);

在构造器 constructor 的参数位置加上访问修饰符

简写:修饰符 + 参数

class User {
  constructor(name, age) {
    this.name = name;
    this.age = age;
    this.gender = "男";
    this.publishNumber = 3;
    this.curNumber = 0;
    this.id = Math.random();
  }
  publish(title) {
    if (this.curNumber < this.publishNumber) {
      console.log(`${title}已发布`);
      this.curNumber++;
    } else {
      console.log("发布已达上线");
    }
  }
}
const u = new User("dahuyou", 23);

编译结果和我们预期是相符的。

7-3. 访问器

前言

本节要介绍的访问器(getter、setter),其实不是 ts 中新增的玩意儿,这东西在 es6 中就已经出现了,这里正好讲解「类」,就顺带提了一嘴。

任务列表

  • 理解 Java 中是如何模拟访问器的效果的
  • 掌握 setter、getter 的用法

notes

访问器

作用:用于控制属性的读写。

class User {
  readonly id: number;
  gender: "男" | "女" = "男";
  pid: string | undefined;
  private publishNumber: number = 3;
  private curNumber: number = 0;
  constructor(public name: string, public age: number) {
    this.id = Math.random();
  }

  publish(title: string) {
    if (this.curNumber < this.publishNumber) {
      console.log(`${title}已发布`);
      this.curNumber++;
    } else {
      console.log("发布已达上线");
    }
  }
}

const u = new User("dahuyou", 23);
u.age = -1; // 负数
u.age = 1.5; // 小数
u.age = 10000; // 偏大
u.age; // 读取 age

基于上一节的代码来介绍访问器。

现在出现了这么一个问题:用户年龄的读写问题。

年龄要求合理,比如:

  1. 年龄不允许是负数;
  2. 年龄不允许成千上万;
  3. 读取到的年龄不允许出现小数;

假设我们现在有以下要求:

  1. 年龄必须得是非负数
  2. 年龄最大值为 200
  3. 读取年龄时,向下取整

若要实现这样的需求,在 java 中,我们会在访问属性时,添加一层方法。

class User {
  readonly id: number;
  gender: "男" | "女" = "男";
  pid: string | undefined;
  private _publishNumber: number = 3;
  private _curNumber: number = 0;
  constructor(public name: string, private _age: number) {
    this.id = Math.random();
  }

  setAge (value: number) {
    if (value < 0) this._age = 0;
    else if (value > 200) this._age = 200;
    else this._age = value;
  }

  getAge () {
    return Math.floor(this._age);
  }

  publish(title: string) {
    if (this._curNumber < this._publishNumber) {
      console.log(`${title}已发布`);
      this._curNumber++;
    } else {
      console.log("发布已达上线");
    }
  }
}

const u = new User("dahuyou", 23);
u.setAge(-1);
u.setAge(1.5);
u.setAge(10000);
u.getAge();

上述做法就是在 Java 中常用的方案 —— 通过调用方法的方式来访问属性,由于在访问属性时要经过中间一层 - 方法,我们就可以在方法中做一些我们想做的事儿。

这种实现方式是完全没有问题的,唯一不妥的地方就是我们在访问属性的时候,写起来不那么方便。
比如给私有属性 _age 赋值,需要调用 setAge 方法u.setAge(xxx),读取需要调用 getAge 方法u.getAge()

操作 现在的写法 我们希望的写法
u.getAge() u.age
u.setAge(xxx) u.age = xxx

为了达到我们希望的效果,访问器属性就派上用场了。

  • set,表示设置器 - 写法:set 属性 (value) { xxx }
  • get,表示读取器 - 写法:get 属性 () { return xxx }
class User {
  readonly id: number;
  gender: "男" | "女" = "男";
  pid: string | undefined;
  private _publishNumber: number = 3;
  private _curNumber: number = 0;
  constructor(public name: string, private _age: number) {
    this.id = Math.random();
  }

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

  publish(title: string) {
    if (this._curNumber < this._publishNumber) {
      console.log(`${title}已发布`);
      this._curNumber++;
    } else {
      console.log("发布已达上线");
    }
  }
}

const u = new User("dahuyou", 23);
u.age = -1; // u.setAge(-1);
u.age = 1.5; // u.setAge(1.5);
u.age = 10000; // u.setAge(10000);
u.age; // u.getAge();

效果上和 Java 那种写法是完全等效的。

但是采用这种写法,能够让我们访问属性 _age的时候更加自然。

:::warning ⚠️ 千万不要在访问器中去读取或设置 age,以免陷入无限递归。

在应用访问器的时候,我们通常也都是要借助私有属性来完成的。 :::

Q & A

🤔 如果我们仅定义了读取器,没有设置器,那么 _age 是不是就自动变成了 read-only 啦? 是的

如果我们通过下面这种方式来使用访问器,还可以模拟 readonly 关键字的效果。

image.png

🤔 访问器是 ts 中特有的吗? 千万别这么认为,这其实压根就不是 ts 给我们提供的,es 中就有了。我们在 js 文件中也可以直接使用访问器。

image.png

看编译结果就知道了,访问器在 index.js 文件中依旧是存在的。

7-4. 练习:增加洗牌和发牌功能

前言

任务列表

  • 独立完成练习

写得比较吃力,还是不习惯写 class

袁老版本

export enum Color {
  heart = "♥",
  spade = "♠",
  club = "♣",
  diamond = "♦",
}

export 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",
}
import { Color, Mark } from "./enums";

export interface Card {
  getString(): string;
}

export interface NormalCard extends Card {
  color: Color;
  mark: Mark;
}

export interface Joker extends Card {
  type: "big" | "small";
}
import { Card, Joker } from "./types";
import { Mark, Color } from "./enums";

interface PublishResult {
  player1: Deck;
  player2: Deck;
  player3: Deck;
  left: Deck;
}

export class Deck {
  private cards: Card[] = [];

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

  // 洗牌
  shuffle() {
    // [x1,x2,x3,x4,x5,x6,x7]
    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;
    }
  }

  //发完牌后,得到的结果有4个card[]
  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,
    };
  }

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

  // 获取一个随机数(不包括最大值)
  private getRandom(min: number, max: number) {
    const dec = max - min;
    return Math.floor(Math.random() * dec + min);
  }
}

cards.push(this.cards.shift() as Card);

image.png

取数组的第一项,有可能会取到 undefined

image.png

如果数组为空,那么会返回 undefined

import { Deck } from "./deck";

const deck = new Deck();
deck.shuffle();
console.log("=========洗牌之后=======");
deck.print();
const result = deck.publish();
console.log("=========发牌之后=======");

console.log("===========玩家1========");
result.player1.print();

console.log("===========玩家2========");
result.player2.print();

console.log("===========玩家3========");
result.player3.print();

console.log("===========桌面========");
result.left.print();

我的版本

根据自己的理解,新增功能:按照牌面大小整理手牌。

// 黑桃(Spade)、红桃(Heart)、方块(Diamond)、梅花(Club)
export enum Colors { // 花色
  Heart = "❤",
  Spade = "♠",
  Club = "♣",
  Diamond = "◇",
}

// one,two,three,four,five,six,seven,eight,nine,ten,eleven,twelve,thirteen
// 牌编号
export 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",
}

本想将 level,牌面大小封装在 enums 里面,改写[1]为[2],结果发现这个枚举貌似不能这么写。。。 image.pngimage.png

import { Colors, Nums } from "./enums";

export interface Poker { // 一张牌
  getString: () => void
  level: number // 牌面大小
}

export interface JokerPoker extends Poker { // 一张大、小王
  type: "big" | "small"
}

export interface NormalPoker extends Poker { // 一张普通的牌
  number: Nums;
  color: Colors;
};

export type Pokers = Poker[]; // 若干张扑克
import { Colors, Nums } from "./enums";
import { JokerPoker, NormalPoker, Poker, Pokers } from "./types";

interface publishResult {
  player1: Deck; // 玩家1手牌
  player2: Deck; // 玩家2手牌
  player3: Deck; // 玩家3手牌
  desk: Deck; // 底牌
}

export class Deck {
  cards: Pokers = [];

  constructor(cards?: Pokers) {
    if (cards) this.cards = cards;
    else this.init();
  }

  // 初始化一副牌
  init() {
    // 插入大小王
    let jo: JokerPoker = {
        type: "small",
        getString() {
          return "joker";
        },
        level: 15,
      },
      JO: JokerPoker = {
        type: "big",
        getString() {
          return "JOKER";
        },
        level: 16,
      };
    this.cards.push(jo, JO);
    // 插入 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++) {
        this.cards.push({
          number: Nums[numKeys[j]],
          color: Colors[colorKeys[i]],
          getString() {
            return `${this.color}${this.number}`;
          },
          level: j !== 0 && j !== 1 ? j + 1 : j === 0 ? 13 : 14,
          // 数字 1 和 2 比较特殊,2 是 13 个数字中最大的,1 是第二大的
        } as NormalPoker);
      }
    }
    this.cards.sort(() => Math.random() - 0.5); // 打乱牌序
  }

  // 打印
  print() {
    let result = "";
    this.cards.forEach((poker) => (result += poker.getString() + "  "));
    console.log(result);
  }

  // 洗牌
  shuffle() {
    const len = this.cards.length;
    for (let i = 0; i < len; i++) {
      const targetIndex = this.getRandomIndex(0, len);
      // 交换 i、targetIndex
      [this.cards[i], this.cards[targetIndex]] = [
        this.cards[targetIndex],
        this.cards[i],
      ];
    }
  }

  // 发牌
  publish(): publishResult {
    const player1 = this.takeCards(17);
    const player2 = this.takeCards(17);
    const player3 = this.takeCards(17);
    const desk = new Deck(this.cards); // 剩下的 3 张底牌
    return {
      player1,
      player2,
      player3,
      desk,
    };
  }

  // 按照牌面大小整理手牌
  arrangeCards() {
    this.cards.sort((a, b) => a.level - b.level);
  }

  // 拿牌
  private takeCards(len: number): Deck {
    const cards: Pokers = [];
    for (let i = 0; i < len; i++) {
      cards.push(this.cards.pop() as Poker);
    }
    return new Deck(cards);
  }

  // 获取一个随机数,不包括最大值
  private getRandomIndex(min: number, max: number) {
    return Math.floor((max - min) * Math.random() + min);
  }
}
import { Deck } from "./deck"

console.log('------初始化一副牌------');
const deck = new Deck();

console.log('------开始洗牌------');
deck.shuffle();

console.log('------开始发牌------');
const publishResult = deck.publish();

console.log('玩家1 手牌:');
publishResult.player1.print();

console.log('玩家2 手牌:');
publishResult.player2.print();

console.log('玩家3 手牌:');
publishResult.player3.print();

console.log('底牌:');
publishResult.desk.print();

console.log('------玩家3 安牌面大小整理手牌------');
publishResult.player3.arrangeCards();
console.log('玩家3 手牌:');
publishResult.player3.print();