用 JavaScript 實現基於類的枚舉模式

做者: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,
};

這種實現有幾個問題:安全

  1. 日誌輸出:若是你輸出一個枚舉值,例如 Color.red,是看不到它的名稱的。
  2. 類型安全:枚舉值不是惟一的,它們會其餘數字所幹擾。例如,數字 1 可能會誤認爲 Color.green,反之亦然。
  3. 成員資格檢查:你沒法輕鬆檢查給定的值是否爲 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);

經過 switch 使用枚舉值

枚舉模式也有其缺點:一般在建立枚舉時不能引用其餘的枚舉(由於這些枚舉可能還不存在)。解決方法是,能夠經過如下函數在外部實現輔助函數:

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 獲取實例的枚舉值

另外一個解決在聲明枚舉時沒法使用其餘枚舉的方法是經過 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);

本文首發微信公衆號:前端先鋒

歡迎掃描二維碼關注公衆號,天天都給你推送新鮮的前端技術文章

歡迎掃描二維碼關注公衆號,天天都給你推送新鮮的前端技術文章

歡迎繼續閱讀本專欄其它高贊文章:


相關文章
相關標籤/搜索