前言

关键字
静态成员、this 指向、单例模式

参考资料

notes

静态成员

什么是静态成员?
静态成员是指:附着在类上的成员(属于某个类的成员)

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

静态成员是通过类去调用的,而实例成员是通过实例去调用的。

静态成员并非是 ts 给我们整出来的一个东西,在 es 中就已经有了,只不过在 ts 中,对静态成员的一些操作添加了一些强约束,预防我们写出高隐患的代码。

static 关键字
使用 static 关键字修饰的成员为静态成员。

  1. console.log(Number.MAX_VALUE);
  2. // => 1.7976931348623157e+308

**Number.MAX_VALUE**
MAX_VALUE 就是类 Number 身上的一个静态成员属性。
静态成员只能通过类来访问。

Number 类身上除了含有 MAX_VALUE 这个静态成员之外,还有很多其它的静态成员。
在 vscode 编辑器中,我们只要输入 Number. 就能查看它身上所有的静态成员。
image.png

  1. class User {
  2. constructor(
  3. public loginId: string,
  4. public loginPwd: string,
  5. public name: string,
  6. public age: number
  7. ) {}
  8. /**
  9. * 登录方法
  10. * @param loginId 账号
  11. * @param loginPwd 密码
  12. * @returns 成功返回用户对象,失败返回 undefined
  13. */
  14. login(loginId: string, loginPwd: string): User | undefined {
  15. // ...
  16. return undefined;
  17. }
  18. }
  19. const u = new User("admin", "123123", "dahuyou", 23);
  20. // u.login();

login,是登录方法,它不应该作为一个实例方法,而应该作为一个静态成员方法。

分析
在注册时,需要新建一个用户对象,使用 User 类来实例化一个用户实例。(用户对象从没有到有)
在登录时,需要调用一个 login 方法,此时并不需要实例化新的用户对象。(检查该用户对象是否已存在)
每次登录都要先新建一个用户对象,这样的逻辑显然是不合理的,用户对象的创建,应该是在注册新用户的时候发生的事儿。

this 指向

class User {
  static users: User[] = []; // 存放所有已注册的用户实例

  constructor(
    public loginId: string,
    public loginPwd: string,
    public name: string,
    public age: number
  ) {
    // this -> 当前用户实例 -> new User()
    User.users.push(this);
  }

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

const u1 = new User("admin1", "123123", "dahuyou", 23);
const u2 = new User("admin2", "123123", "dahuyou", 23);
const u3 = new User("admin3", "123123", "dahuyou", 23);
// User.users.push(u1, u2, u3);

console.log(User.login("admin1", "123123")); // => User 实例
console.log(User.login("admin2", "123123")); // => User 实例
console.log(User.login("admin3", "123123")); // => User 实例
console.log(User.login("admin4", "123123")); // => undefined

image.png

🤔 users 更应该是一个实例成员还是静态成员?
答:静态成员。

users 属性存放的是所有已成功注册的用户。下面分析「如果将 users 定义为一个实例成员」为什么不合理:

**u1.users === u2.users === u3.users**
通过任一实例来访问 users,得到的将会是同一个 users。
这么设计显然是不合理的,因为每一个实例身上的东西通常都是独立的,对于这个相同的属性,应该将其定义为一个静态成员更加合理。

  1. 我们想要知道当前已经成功注册多少人
  2. 获取到 User 实例

2 不应该是 1 的前提条件,即便没有实例化 User 对象,我们应该也是可以获取到当前已经成功注册的成员的。

每新建一个用户实例,就将其丢到 users 中:

  • 每次创建好实例后,手动 push:User.users.push(u1, u2, u3)「不推荐」
  • 统一封装在构造器里面:User.users.push(this)「推荐」

此时 this 指向的就是当前创建的对象。

this 指向
在静态成员(属性、方法)中,this 指向当前类
在实例方法、构造器中,this 指向当前对象
在静态方法 static login 中,this.users 等效于 User.users

class User {
  constructor(loginId, loginPwd, name, age) {
    this.loginId = loginId;
    this.loginPwd = loginPwd;
    this.name = name;
    this.age = age;
    User.users.push(this);
  }
  static login(loginId, loginPwd) {
    return this.users.find((u) => u.loginId === loginId && u.loginPwd === loginPwd);
  }
}
User.users = [];
const u1 = new User("admin1", "123123", "dahuyou", 23);
const u2 = new User("admin2", "123123", "dahuyou", 23);
const u3 = new User("admin3", "123123", "dahuyou", 23);
console.log(User.login("admin1", "123123"));
console.log(User.login("admin2", "123123"));
console.log(User.login("admin3", "123123"));
console.log(User.login("admin4", "123123"));

static 关键字,在 es 中是存在的。
并且在 ts 和 js 中,它的用法是一致的。

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

表示的含义都是一样的,都是在直接扩展 User 类。

设计模式 - 单例模式

单例模式:某些类的对象,在系统中最多只能有一个,为了避免开发者犯错 —— 实例化多个该类的对象,可以使用单例模式进行强约束。

下面以中国象棋小游戏为例,有这么一个 Board 类,用于创建一个棋盘。
但是棋盘只需要一个即可,我们只需要创建一次。

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

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

const b1 = new Board();
const b2 = new Board(); // 重复了
b1 === b2; // => false

如果采用上面这种写法来实现棋盘类的初始化,那么可以创建多个不同的棋盘,而这并不是我们希望看到的。

我们可以采用单例模式来改写上述代码。

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

  // 1
  private constructor() {}

  // 2
  private static _board?: Board;

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

const b1 = Board.createBoard();
const b2 = Board.createBoard();
b1 === b2; // => true

步骤:

  1. 将构造函数私有化
  2. 准备一个私有变量 —— 系统中唯一的棋盘对象
  3. 准备一个静态成员方法 —— 创建唯一实例(棋盘对象),并保存到上一步创建的私有变量中

image.png
当我们将构造函数私有化之后,我们就没法通过 new 关键字来调用类,实例化对象了。

上面介绍的实现步骤并不是死的,也有其它实现写法。

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

  private constructor() {}

  static readonly singleBoard = new Board();
}

const b1 = Board.singleBoard;
const b2 = Board.singleBoard;
b1 === b2 // => true

readonly
加上 readonly 的目的是为了防止给Board.singleBoard重新赋值。

在这个例子中,即便不加 readonly 也无法给 Board.singleBoard重新赋值 当我们在初始化singleBoard时,通过类型推导,确定了它的类型是 Board 类型 如果想要给它重新赋值,那么我们必须得调用 new Board() 因为我们已经将构造函数私有化,所以new Board()没法在类 Board 之外调用

对比「写法1」和「写法2」

棋盘实例的创建
写法1:我们需要手动调用Board.createBoard()去创建棋盘
写法2:自动就会创建好棋盘Board.singleBoard

如果我们在创建棋盘时,还有一些其它逻辑需要处理,那么使用「写法1」更加合理。