做者:Dr. Axel Rauschmayerhtml
翻譯:瘋狂的技術宅前端
原文:2ality.com/2020/01/enu…git
未經容許嚴禁轉載github
在本文中,咱們將會研究在 JavaScript 中實現基於類的枚舉模式。還會研究一下 Enumify 這個可以幫助咱們使用枚舉模式的庫。前端工程化
枚舉是由一組值組成的類型。例如 TypeScript 中有內置的枚舉,咱們能夠經過它們來定義本身的布爾類型:安全
enum MyBoolean {
false,
true,
}
複製代碼
或者能夠定義本身的顏色類型:函數
enum Color {
red,
orange,
yellow,
green,
blue,
purple,
}
複製代碼
這段 TypeScript 代碼會被編譯爲如下 JavaScript 代碼(省略了一些詳細信息,以便於理解):工具
const Color = {
red: 0,
orange: 1,
yellow: 2,
green: 3,
blue: 4,
purple: 5,
};
複製代碼
這種實現有幾個問題:測試
Color.red
,是看不到它的名稱的。1
可能會誤認爲 Color.green
,反之亦然。Color
的元素。用普通 JavaScript,咱們能夠經過使用字符串而不是數字做爲枚舉值來解決問題 1:ui
const Color = {
red: 'red',
orange: 'orange',
yellow: 'yellow',
green: 'green',
blue: 'blue',
purple: 'purple',
}
複製代碼
若是咱們用符號做爲枚舉值,還可以得到類型安全性:
const Color = {
red: Symbol('red'),
orange: Symbol('orange'),
yellow: Symbol('yellow'),
green: Symbol('green'),
blue: Symbol('blue'),
purple: Symbol('purple'),
}
assert.equal(
String(Color.red), 'Symbol(red)');
複製代碼
符號存在的一個問題是須要將它們明確轉換爲字符串,而不能強制轉換(例如,經過 +
或內部模板文字):
assert.throws(
() => console.log('Color: '+Color.red),
/^TypeError: Cannot convert a Symbol value to a string$/
);
複製代碼
儘管能夠測試成員資格,但這並不簡單:
function isMember(theEnum, value) {
return Object.values(theEnum).includes(value);
}
assert.equal(isMember(Color, Color.blue), true);
assert.equal(isMember(Color, 'blue'), false);
複製代碼
經過對枚舉使用自定義類能夠使咱們進行成員資格測試,並在枚舉值方面具備更大的靈活性:
class Color {
static red = new Color('red');
static orange = new Color('orange');
static yellow = new Color('yellow');
static green = new Color('green');
static blue = new Color('blue');
static purple = new Color('purple');
constructor(name) {
this.name = name;
}
toString() {
return `Color.${this.name}`;
}
}
複製代碼
我把這種用類做爲枚舉的方式稱爲「枚舉模式」。它受到 Java 中對枚舉實現的啓發。
輸出:
console.log('Color: '+Color.red);
// Output:
// 'Color: Color.red'
複製代碼
成員資格測試:
assert.equal(
Color.green instanceof Color, true);
複製代碼
Enumify 是一個可以幫助咱們使用枚舉模式的庫。它的用法以下:
class Color extends Enumify {
static red = new Color();
static orange = new Color();
static yellow = new Color();
static green = new Color();
static blue = new Color();
static purple = new Color();
static _ = this.closeEnum();
}
複製代碼
Enumify 可以把多個實例屬性添加到枚舉值中:
assert.equal(
Color.red.enumKey, 'red');
assert.equal(
Color.red.enumOrdinal, 0);
複製代碼
用 Enumify 實現 .toStrin()
:
assert.equal(
'Color: ' + Color.red, // .toString()
'Color: Color.red');
複製代碼
Enumify 設置了兩個靜態屬性– .enumKeys
和 .enumValues
:
assert.deepEqual(
Color.enumKeys,
['red', 'orange', 'yellow', 'green', 'blue', 'purple']);
assert.deepEqual(
Color.enumValues,
[ Color.red, Color.orange, Color.yellow,
Color.green, Color.blue, Color.purple]);
複製代碼
它提供了可繼承的靜態方法 .enumValueOf()
:
assert.equal(
Color.enumValueOf('yellow'),
Color.yellow);
複製代碼
它實現了可繼承的可迭代性:
for (const c of Color) {
console.log('Color: ' + c);
}
// Output:
// 'Color: Color.red'
// 'Color: Color.orange'
// 'Color: Color.yellow'
// 'Color: Color.green'
// 'Color: Color.blue'
// 'Color: Color.purple'
複製代碼
class Weekday extends Enumify {
static monday = new Weekday(true);
static tuesday = new Weekday(true);
static wednesday = new Weekday(true);
static thursday = new Weekday(true);
static friday = new Weekday(true);
static saturday = new Weekday(false);
static sunday = new Weekday(false);
static _ = this.closeEnum();
constructor(isWorkDay) {
super();
this.isWorkDay = isWorkDay;
}
}
assert.equal(Weekday.sunday.isWorkDay, false);
assert.equal(Weekday.wednesday.isWorkDay, true);
複製代碼
枚舉模式也有其缺點:一般在建立枚舉時不能引用其餘的枚舉(由於這些枚舉可能還不存在)。解決方法是,能夠經過如下函數在外部實現輔助函數:
class Weekday extends Enumify {
static monday = new Weekday();
static tuesday = new Weekday();
static wednesday = new Weekday();
static thursday = new Weekday();
static friday = new Weekday();
static saturday = new Weekday();
static sunday = new Weekday();
static _ = this.closeEnum();
}
function nextDay(weekday) {
switch (weekday) {
case Weekday.monday:
return Weekday.tuesday;
case Weekday.tuesday:
return Weekday.wednesday;
case Weekday.wednesday:
return Weekday.thursday;
case Weekday.thursday:
return Weekday.friday;
case Weekday.friday:
return Weekday.saturday;
case Weekday.saturday:
return Weekday.sunday;
case Weekday.sunday:
return Weekday.monday;
default:
throw new Error();
}
}
複製代碼
另外一個解決在聲明枚舉時沒法使用其餘枚舉的方法是經過 getter 延遲訪問同級的值:
class Weekday extends Enumify {
static monday = new Weekday({
get nextDay() { return Weekday.tuesday }
});
static tuesday = new Weekday({
get nextDay() { return Weekday.wednesday }
});
static wednesday = new Weekday({
get nextDay() { return Weekday.thursday }
});
static thursday = new Weekday({
get nextDay() { return Weekday.friday }
});
static friday = new Weekday({
get nextDay() { return Weekday.saturday }
});
static saturday = new Weekday({
get nextDay() { return Weekday.sunday }
});
static sunday = new Weekday({
get nextDay() { return Weekday.monday }
});
static _ = this.closeEnum();
constructor(props) {
super();
Object.defineProperties(
this, Object.getOwnPropertyDescriptors(props));
}
}
assert.equal(
Weekday.friday.nextDay, Weekday.saturday);
assert.equal(
Weekday.sunday.nextDay, Weekday.monday);
複製代碼
getter 傳遞給對象內部的構造函數。構造函數經過 Object.defineProperties() 和 Object.getOwnPropertyDescriptors()
將它們複製到當前實例。可是咱們不能在這裏使用 Object.assign()
,由於它沒法複製 getter 和其餘方法。
在下面的例子中實現了一個狀態機。咱們將屬性(包括方法)傳遞給構造函數,構造函數再將其複製到當前實例中。
class State extends Enumify {
static start = new State({
done: false,
accept(x) {
if (x === '1') {
return State.one;
} else {
return State.start;
}
},
});
static one = new State({
done: false,
accept(x) {
if (x === '1') {
return State.two;
} else {
return State.start;
}
},
});
static two = new State({
done: false,
accept(x) {
if (x === '1') {
return State.three;
} else {
return State.start;
}
},
});
static three = new State({
done: true,
});
static _ = this.closeEnum();
constructor(props) {
super();
Object.defineProperties(
this, Object.getOwnPropertyDescriptors(props));
}
}
function run(state, inputString) {
for (const ch of inputString) {
if (state.done) {
break;
}
state = state.accept(ch);
console.log(`${ch} --> ${state}`);
}
}
複製代碼
狀態機檢測字符串中是否存在連續的三個 1
的序列:
run(State.start, '01011100');
// Output:
// '0 --> State.start'
// '1 --> State.one'
// '0 --> State.start'
// '1 --> State.one'
// '1 --> State.two'
// '1 --> State.three'
複製代碼
有時咱們須要枚舉值是數字(例如,用於表示標誌)或字符串(用於與 HTTP 頭中的值進行比較)。能夠經過枚舉來實現。例如:
class Mode extends Enumify {
static user_r = new Mode(0b100000000);
static user_w = new Mode(0b010000000);
static user_x = new Mode(0b001000000);
static group_r = new Mode(0b000100000);
static group_w = new Mode(0b000010000);
static group_x = new Mode(0b000001000);
static all_r = new Mode(0b000000100);
static all_w = new Mode(0b000000010);
static all_x = new Mode(0b000000001);
static _ = this.closeEnum();
constructor(n) {
super();
this.n = n;
}
}
assert.equal(
Mode.user_r.n | Mode.user_w.n | Mode.user_x.n |
Mode.group_r.n | Mode.group_x.n |
Mode.all_r.n | Mode.all_x.n,
0o755);
assert.equal(
Mode.user_r.n | Mode.user_w.n | Mode.user_x.n |
Mode.group_r.n,
0o740);
複製代碼