前端素材 设计元素 界面设计 网页素材 网站模板 按钮素材 名片素材 字体设计 图标设计 生活百科

用 JavaScript 实现基于类的枚举模式

生活百科 7v 1月前  次浏览

实现枚举:第一次尝试

枚举是由一组值组成的类型。例如 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,
};

这种实现有几个问题:

  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);

相关链接

发表评论