做者:Dr. Axel Rauschmayer翻譯:瘋狂的技術宅html
原文:https://2ality.com/2020/01/en...前端
未經容許嚴禁轉載git
在本文中,咱們將會研究在 JavaScript 中實現基於類的枚舉模式。還會研究一下 Enumify 這個可以幫助咱們使用枚舉模式的庫。程序員
枚舉是由一組值組成的類型。例如 TypeScript 中有內置的枚舉,咱們能夠經過它們來定義本身的布爾類型:github
enum MyBoolean { false, true, }
或者能夠定義本身的顏色類型:面試
enum Color { red, orange, yellow, green, blue, purple, }
這段 TypeScript 代碼會被編譯爲如下 JavaScript 代碼(省略了一些詳細信息,以便於理解):segmentfault
const Color = { red: 0, orange: 1, yellow: 2, green: 3, blue: 4, purple: 5, };
這種實現有幾個問題:安全
Color.red
,是看不到它的名稱的。1
可能會誤認爲 Color.green
,反之亦然。Color
的元素。用普通 JavaScript,咱們能夠經過使用字符串而不是數字做爲枚舉值來解決問題 1:服務器
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);