关于TypeScript静态成员的所有信息 | TypeScript OOP

109 阅读5分钟

在面向对象的编程中,我们写了很多的

类包含持有变量和操作的属性(方法和属性)。

每当我们定义一个类的属性时,它们都被说成是属于以下两种情况。

  • 该类的一个实例(通过构造函数创建的对象)或
  • 本身(我们称之为类成员)

我们这样做是什么意思?

属性怎么能只属于实例与只属于呢?

当我们选择使用或省略static 关键字时,就会改变属性的归属者。

让我们看看没有static 关键字的常规用法。

常规用法(属性属于实例)

通常,当我们在一个类上定义属性时,只有在我们创建了该类的实例之后才能访问它们,或者我们使用this 来引用最终将驻留在对象的实例上的属性。

以这个来自白色标签的早期例子为例。

type Genre = 'rock' | 'pop' | 'electronic' | 'rap'

class Vinyl {
  public title: string;
  public artist: string;
  public genres: Genre[];

  constructor (title: string, artist: string, genres: Genre[]) {
    this.title = title;
    this.artist = artist;
    this.genres = genres;
  } 

  public printSummary (): void {
    console.log(`${this.title} is an album by ${this.artist}`);
  }
}

const vinyl = new Vinyl('Goo', 'Sonic Youth', ['rock']);
console.log(vinyl.title)    // 'Goo'
console.log(vinyl.artist)   // 'Sonic Youth'
console.log(vinyl.genres)   // ['rock']
vinyl.printSummary();	      // 'Goo is an album by Sonic Youth'

Vinyl 类上的每个方法 (printSummary(): void) 和属性 (title,artist,genres) 都被认为属于该类的一个实例

在这个例子中,我们只能在对象被创建直接访问属性title,artistgenres

console.log(vinyl.title)    // This is valid!

还要注意的是,当我们使用printSummary(): void ,我们可以使用this 关键字访问titleartist

class Vinyl {
  ...
  public printSummary (): void {
    console.log(`${this.title} is an album by ${this.artist}`);
  }
}

这样做是因为此时,生成的对象/Vinyl 的实例拥有这些属性。

如果我们查看TypeScript Playground,我们可以看一下这个代码样本的编译的JavaScript。

"use strict";
class Vinyl {
  constructor(title, artist, genres) {
    this.title = title;
    this.artist = artist;
    this.genres = genres;
  }
  printSummary() {
    console.log(`${this.title} is an album by ${this.artist}`);
  }
}

const vinyl = new Vinyl('Goo', 'Sonic Youth', ['rock']);
console.log(vinyl.title); // 'Goo'
console.log(vinyl.artist); // 'Sonic Youth'
console.log(vinyl.genres); // ['rock']
vinyl.printSummary(); // 'Goo is an album by Sonic Youth'

结果的JavaScript看起来几乎是一样的

让我们谈一谈当属性被所拥有时,会发生什么。

静态属性(属性属于类)。

当我们在类上定义的属性上使用static 关键字时,它们就属于这个类本身

这意味着我们不能从该类的实例中访问这些属性。

我们只能通过引用类本身来直接访问这些属性。

为了证明这一点,让我们添加一个计数器NUM_VINYL_CREATED ,以增加一个Vinyl 被创建的次数。

type Genre = 'rock' | 'pop' | 'electronic' | 'rap'

class Vinyl {
  public title: string;
  public artist: string;
  public genres: Genre[];
  public static NUM_VINYL_CREATED: number = 0;

  constructor (title: string, artist: string, genres: Genre[]) {
    this.title = title;
    this.artist = artist;
    this.genres = genres;

	Vinyl.NUM_VINYL_CREATED++;        // increment number of vinyl created
    console.log(Vinyl.NUM_VINYL_CREATED)  
  } 

  public printSummary (): void { 
    console.log(`${this.title} is an album by ${this.artist}`);
  }
}

let goo = new Vinyl ('Goo', 'Sonic Youth', ['rock']);
// prints out 1

let daydream = new Vinyl ('Daydream Nation', 'Sonic Youth', ['rock']);
// prints out 2

因为属性只能通过引用类名本身来访问,所以我们不能像这样通过一个实例 来访问静态属性

let goo = new Vinyl ('Goo', 'Sonic Youth', ['rock']);
goo.MAX_GENRES_PER_VINYL    // Error
goo.NUM_VINYL_CREATED       // Error

你可能听说过一个叫做类成员的术语。一个属性或一个方法是一个类成员,因为它们只能通过类本身来访问;因此,它们是类的成员。

class Vinyl {

  /**
   * Example "Class Member" (sometimes known as 
   * "Static Class Member") variable.
   * 
   * Can only be accessed through mentioning the class name
   * itself:
   * 
   * Example: const vinylCreatedToDate = Vinyl.NUM_VINYL_CREATED;
   */

  public static NUM_VINYL_CREATED: number = 0; 

  /**
   * Example "Instance Member" variable.
   * 
   * Can only be accessed through an instance/object created from 
   * this class.
   * 
   * Example: 
   *  
   *  const blueAlbum = new Vinyl("Blue Album");
   *  console.log(blueAlbum.title); // "Blue Album"
   */

  public title: string;

  constructor (title: string) {
    this.title = title;
  }
}

不同类型的成员变量可以出现在一个类上。

这很好,但你什么时候会想使用静态属性呢?

如何知道何时使用静态属性

在你添加那个属性或方法之前,问问你自己。

这个属性是否需要被另一个类使用,而不需要首先创建它的一个对象?

换句话说,我是否需要在这个类所创建的对象上调用它?如果是,那就继续正常使用。

如果不是,那么你可能要做一个static 成员。

使用静态属性有意义的场景

看起来有意义但实际上会导致领域模型贫乏的情况。

  • 对该类的属性执行验证逻辑(使用价值对象代替)。

为了演示一个有价值的场景,让我们添加一个static MAX_GENRES_PER_VINYL 属性来 "记录一个约束",即一个Vinyl 最多只能有两个不同类型的Genres

type Genre = 'rock' | 'pop' | 'electronic' | 'rap'

class Vinyl {
  public title: string;
  public artist: string;
  public genres: Genre[];
  public static MAX_GENRES_PER_VINYL: number = 2;

  constructor (title: string, artist: string, genres: Genre[]) {
    this.title = title;
    this.artist = artist;
    this.genres = genres;
  }

  public printSummary (): void { 
    console.log(`${this.title} is an album by ${this.artist}`);
  }
}

然后让我们添加一个addGenre(genre: Genre): void 方法来执行这个业务规则。

type Genre = 'rock' | 'pop' | 'electronic' | 'rap'

class Vinyl {
  public title: string;
  public artist: string;
  public genres: Genre[];
  public static MAX_GENRES_PER_VINYL: number = 2;

  constructor (title: string, artist: string, genres: Genre[]) {
    this.title = title;
    this.artist = artist;
    this.genres = genres;
  }

  public addGenre (genre: Genre): void {
    // Notice that in order to reference the value, we have go through the class
    // itself (Vinyl), not through an instance of the class (this).
    const maxLengthExceeded = this.genres.length >= Vinyl.MAX_GENRES_PER_VINYL;
    const alreadyAdded = this.genres.filter((g) => g === genre).length !== 0;

    if (!maxLengthExceeded && !alreadyAdded) {
      this.genres.push(genre);
    }
  }

  public printSummary (): void { 
    console.log(`${this.title} is an album by ${this.artist}`);
  }
}