Javascript Enum 枚举模式

32,162 阅读2分钟

编码中使用的枚举类型, 多半是为了便于代码提示和开发方便, 能快速查看包含的成员. 比如有些第三方库 keymirror, 开发中遇到了一个有趣的库, 他的一些用法可以借鉴, Enumify, 本文部分内容翻译自其官方文档

下面来对枚举模式进行一些探讨.

常见版本

在 JavaScript 中没有 enum 类型, typescript 有, 比如

enum Color {
    red,
    yellow,
    blue
}

当这一段 ts 被 compile 成 js 的时候, 就会是下面这个样子

const Color = {
    red: 0,
    yellow: 1,
    blue: 2
}

这是最常见的枚举形式, 但是有以下的问题

  1. 对打印不友好, 如果 console.log(Color.red)会只有一个数字
  2. 类型不安全, 枚举值不唯一, 可以被其他数字干扰. 比如我可以再次设置Color.blue为1, 就会造成与Color.red的冲突
  3. 成员无法检查. 没办法检查给出的值是否是Color的成员

那么很自然而然的想到另外一种方法

const Color = {
    red: 'red',
    yellow: 'yellow',
    blue: 'blue'
}

甚至于为了唯一性

const Color = {
    red: Symbol('red'),
    yellow: Symbol('yellow'),
    blue: Symbol('blue')
}

这里顺便提一下, Symbol 是无法转换成字符串的, 也就是说'' + Symbol('red')Symbol('red').toString()都是错误的用法.

assert.equal(String(Symbol('red')) === 'Symbole(red)')

那么如何去判断成员呢?

function checkMember(enumObject, value) {
    return Object.values(enumObject).includes(value);
}

基于类版本

使用自定义类可以满足更好的成员检测以及枚举值的灵活性

class Color {
    static red = new Color('red')
    static yellow = new Color('yellow')
    static blue = new Color('blue')
    static fakeRed = new Color('red')
    constructor(name) {
        this.name = name
    }
    toString() {
        return `Color.${this.name}`
    }
}

这种语法很像 Java 里的枚举类型声明 看看上面列举的三个问题:

  1. 打印
console.log('Color: ', Color.red) // Color: Color.red
  1. 枚举值唯一性: 至少都是新对象, 比如fakeRed, 那也是唯一的.
  2. 成员检测
Color.green instanceof Color // true

Enumify 一个枚举的辅助库

更详细的可参考其 github 的 document, 这里只做其核心功能, 以及自己工作中用到的进行展示

class Color extends Enumify {
    static red = new Color()
    static yellow = new Color()
    static blue = new Color()
}

实例属性

console.log(Color.red.enumKey === 'red') // true, 自动添加属性名到枚举 key 
console.log(Color.red.enumOrigin === 0) // true, 表示原始枚举值

原型方法

其实现了.toString()

console.log('Color: ', Color.red) // Color: Color.red

静态属性

enumKeys 所有枚举 key 数组 和 enumValues 所有枚举值数组

Color.enumKeys // ['red', 'yellow', 'blue']
Color.enumValues // [Color.red, Color.yellow, Color.blue]

继承的静态方法: enumValueOf, 根据 key 获取 枚举值

Color.enumValueOf('yellow') // Color.yellow

由上, 可能你已经猜到, 也可以实现迭代

for(let c of Color) {
    console.log(c) // Color.red    Color.yellow     Color.blue
}

实际举例

包含实例属性的枚举值

工作日的例子

class WeedDays extends Enumify {
    static monday = new WeekDays(true)
    static tuesday = new WeekDays(true)
    static wednesday = new WeekDays(true)
    static thursday = new WeekDays(true)
    static friday = new WeekDays(true)
    static sarturday = new WeekDays(true)
    static sunday = new WeekDays(true)
    
    constructor(isWorkDay) {
        super(isWorkDay) // 注意调用父级构造函数
        this.isWorkDay = isWorkDay
    }
}
console.log(WeekDays.monday.isWorkDay) // true

根据枚举值进行切换

function nextDay(weekDay) {
    switch(weekDay) {
        case WeekDays.monday: 
            return WeekDays.tuesday
        // .....
        case WeekDays.sunday:
            return WeekDays.monday
        default:
            thorw new Error('Must be WeekDays static property')
    }
}

枚举值包含实例 getter

class WeekDays extends Enumify {
    static monday = new WeekDays({
        get nextDay() {return WeekDays.tuesday}
    })
    // ...
    static sunday = new WeekDays({
        get nextDay() {return WeekDays.monday}
    })
    
    constructor(props) {
        super()
        Object.defineProperties(this, Object.getOwnPropertyDescriptors(props))
    }
}
console.log(WeekDays.monday.nextDay) // WeekDays.tuesday

通过实例方法实现状态机

class State extends Enumify {
    static start = new State({
        done:false,
        accept(x){
            if (x === 1) return State.one
            return State.start
        }
    })
    static one = new State({
        done: false,
        accept(x) {
            if (x === 1) return State.two
            return State.start
        }
    })
    static two = new State({
        done: true
    })
    constructor(props) {
        super()
        Object.defineProperties(this, Object.getOwnPropertyDescriptors(props))
    }
}
function run(state, inputString) {
    for (let ch of inputString) {
        if (state.done) break;
        state = state.accept(ch)
        console.log(`${ch} ==> ${state}`)
    }
}
run(State.start, '010111');
// '0 --> State.start'
// '1 --> State.one'
// '0 --> State.start'
// '1 --> State.one'
// '1 --> State.two'
// '1 --> State.three'

日常开发中经常使用 enum 来规避魔数, 让代码更加整洁和规范, 可读性也较高, 希望上文可以帮到你.