在面向对象的编程中,我们写了很多的类。
类包含持有变量和操作的属性(方法和属性)。
每当我们定义一个类的属性时,它们都被说成是属于以下两种情况。
- 该类的一个实例(通过构造函数创建的对象)或
- 类本身(我们称之为类成员)
我们这样做是什么意思?
属性怎么能只属于实例与只属于类呢?
当我们选择使用或省略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,artist 和genres 。
console.log(vinyl.title) // This is valid!
还要注意的是,当我们使用printSummary(): void ,我们可以使用this 关键字访问title 和artist 。
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 成员。
使用静态属性有意义的场景
- 检查来自另一个类的业务规则或约束条件
- 实现一个
factory method,以封装创建一个类的实例所需的复杂性。 - 使用一个
abstract factory,以便创建该类的特定类型的实例。 - 当属性不应该改变时
看起来有意义但实际上会导致领域模型贫乏的情况。
- 对该类的属性执行验证逻辑(使用价值对象代替)。
为了演示一个有价值的场景,让我们添加一个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}`);
}
}