7-1. 概述
面向对象的开发思想,需要慢慢培养,本章介绍的是「类」,类和面向对象是息息相关的。
但是,并非说使用到了类就是使用了面向对象的开发方式,这完全是两码事。面向对象的思维方式,不是一朝一夕可以养成的。后续其它几节的内容,主要介绍的重点将放在:ts 相较于 js,对于「类」的写法,都新增了哪些语法,进行了哪些扩展。而不会过多的介绍「面向对象」这种开发思想。
说简单些,就是本章节的重心将放在:在 ts 中写类和在 js 中写类,都有哪些不同的地方;ts 在 js 的基础上,都进行了哪些扩展。
:::tips 袁老提到,他使用面向对象的开发方式来开发,达到小有成就的程度,花费了将就 3 年的时间。 :::
7-2. 新增的类语法
前言
任务列表
-
strictPropertyInitialization
开启更加严格的属性初始化 - 属性列表
- 属性默认值
- 可选属性
- 只读属性
- 访问修饰符
- public
- private
- 属性简写
notes
定义类的错误写法
错误写法:
class User {
constructor (name: string, age: number) {
this.name = name; // ×
this.age = age; // ×
}
}
在 js 中,上面这种写法是完全 OK 的
在 ts 中,上面这种写法会报错
定义对象的错误写法
const user = {};
user.name = "dahuyou"; // ×
user.age = 23; // ×
上述写法在 ts 中,也是不被允许的。
🤔 上面这些写法在 js 中显得很正常,为什么在 ts 中就报错了呢?
TS 认为:对象身上的属性是固定的,一开始就得提前声明好,不能后续随意动态增加,所以上述写法都是不允许的。
属性列表
使用属性列表来描述类中的属性,提前声明好 class 中都有哪些属性:
class User {
name: string
age: number
constructor (name: string, age: number) {
this.name = name;
this.age = age;
}
}
属性列表不会出现在编译结果中
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 的属性列表中声明了俩属性 name
、age
,但是我们仅对 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"
],
}
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); // ×
现在我们想要设置 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
这种细节,简单了解一下就好,现在回看之前的笔记,才发现原来自己当时记录的这么细。
难怪学习进度这么慢。。。
:::
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);
?:
这种写法,编译结果中会直接忽略掉可选属性。
?:
这种写法,其实就等效于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; // ×
访问修饰符
访问修饰符,可以控制类中某个成员的访问权限。
访问修饰符 | 描述 |
---|---|
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 // ×
前面所写的属性 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);
当我们在类内部访问了私有属性,会发现编辑器 vscode 会令它们高亮显示,依次来提示我们它们有被访问。
u.publish('title-1')
u.publish('title-2')
u.publish('title-3')
u.publish('title-4')
u.publish('title-5')
访问修饰符起到的重要作用
这里简单介绍一个模拟的场景,通过该场景来了解访问修饰符的作用。
可能我们写的这个 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 为我们提供了更加便利的语法糖:访问修饰符 + 参数
简写之前的常规做法:
- 提前声明属性
name: string
- 定义好参数
name: string
- 使用传入的参数给提前声明的属性赋值
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
基于上一节的代码来介绍访问器。
现在出现了这么一个问题:用户年龄的读写问题。
年龄要求合理,比如:
- 年龄不允许是负数;
- 年龄不允许成千上万;
- 读取到的年龄不允许出现小数;
假设我们现在有以下要求:
- 年龄必须得是非负数
- 年龄最大值为 200
- 读取年龄时,向下取整
若要实现这样的需求,在 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 关键字的效果。
🤔 访问器是 ts 中特有的吗? 千万别这么认为,这其实压根就不是 ts 给我们提供的,es 中就有了。我们在 js 文件中也可以直接使用访问器。
看编译结果就知道了,访问器在 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);
取数组的第一项,有可能会取到 undefined
如果数组为空,那么会返回 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],结果发现这个枚举貌似不能这么写。。。
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();