JavaScript中的枚举与ES6

216

我正在用Javascript重建一个旧的Java项目,但发现在JS中没有很好的枚举方式。

我能想到的最好方法是:

const Colors = {
    RED: Symbol("red"),
    BLUE: Symbol("blue"),
    GREEN: Symbol("green")
};
Object.freeze(Colors);

“const”关键字可以防止对“Colors”进行重新赋值,使用“freeze”方法可以防止修改键和值。我使用符号(Symbols)使得“Colors.RED”不等于“0”或者除了它本身以外的任何值。
这种方式有问题吗?有更好的方式吗?

(我知道这个问题有点重复,但所有之前的问答都相当旧了,而且ES6为我们提供了一些新的功能。)


编辑:

另一种解决序列化问题的方法,但我认为仍然存在领域问题:

const enumValue = (name) => Object.freeze({toString: () => name});

const Colors = Object.freeze({
    RED: enumValue("Colors.RED"),
    BLUE: enumValue("Colors.BLUE"),
    GREEN: enumValue("Colors.GREEN")
});

通过使用对象引用作为值,您可以获得与符号相同的避免冲突。

2
这在ES6中是一个完美的方法。你不需要冻结它。 - NiRUS
2
@Nirus,如果你不想让它被修改,那么你需要这样做。 - zerkms
3
你有注意到这个答案吗? - Bergi
4
一个问题我能想到的是:无法与 JSON.stringify() 一起使用此枚举。不能序列化/反序列化 Symbol - le_m
1
@ErictheRed 我已经使用了字符串枚举常量值多年,而且从未遇到任何问题,因为使用Flow(或TypeScript)可以保证比担心避免冲突更多的类型安全。 - Andy
显示剩余5条评论
19个回答

209

这个表述有问题吗?

我没有看到任何问题。

有更好的方式吗?

我会把这两个陈述合并成一个:

const Colors = Object.freeze({
    RED:   Symbol("red"),
    BLUE:  Symbol("blue"),
    GREEN: Symbol("green")
});

如果你不喜欢模板代码,不喜欢重复的 Symbol 调用,当然你也可以编写一个帮助函数 makeEnum,它可以从一个名称列表创建相同的东西。


4
这里不是存在领域问题吗? - user663031
3
你的意思是,当代码被加载两次时,它会生成不同的符号,而这对于字符串来说不是问题?是的,很好的观点,把它作为答案吧 :-) - Bergi
2
@ErictheRed 不,Symbol.for没有跨域问题,但它确实存在与真正的全局命名空间的冲突问题。 - Bergi
2
@Sky 在 Colors 查找中的默认值与枚举定义无关。通常可以使用 Colors[name] || Colors.BLUEColors.hasOwnProperty(name) ? Colors[name] : Colors.BLUE 来实现。 - Bergi
2
@ZachSmith 这样可以为将来版本的语言赋予意义,而不会破坏现有的代码。 - Bergi
显示剩余14条评论

44

虽然在简单的用例中使用Symbol作为枚举值可以正常工作,但给枚举赋予属性可能会很方便。这可以通过将包含属性的Object用作枚举值来实现。

例如,我们可以为每个Colors赋予名称和十六进制值:

/**
 * Enum for common colors.
 * @readonly
 * @enum {{name: string, hex: string}}
 */
const Colors = Object.freeze({
  RED:   { name: "red", hex: "#f00" },
  BLUE:  { name: "blue", hex: "#00f" },
  GREEN: { name: "green", hex: "#0f0" }
});

在枚举中包含属性可以避免编写switch语句(并在扩展枚举时可能忘记为switch语句添加新情况)。该示例还显示了使用JSDoc枚举注释记录的枚举属性和类型。
相等性按预期工作,Colors.RED === Colors.REDtrueColors.RED === Colors.BLUEfalse

5
请注意,Object.freeze不是深度冻结,例如您可以更改Colors.RED.name =“charlie”; 您可以使用以下内容来冻结嵌套对象:const deepFreeze = obj => { Object.keys(obj).forEach(prop => { if (typeof obj[prop] === 'object') deepFreeze(obj[prop]); }); return Object.freeze(obj); }; - Stonetip

16

更新于2020年11月05日:
修改后包括静态字段和方法,以更接近“真正”的枚举行为。

如果您计划进行更新,我建议尝试使用我所谓的“Enum Class”(除非您无法接受任何浏览器或运行时环境的限制)。它基本上是一个非常简单和干净的类,使用私有字段和有限访问器来模拟枚举的行为。当我想要在C#中构建更多功能时,我有时会这样做。

我意识到私有类字段目前仍处于实验阶段,但似乎对于创建具有不可变字段/属性的类而言是有效的。浏览器支持也相当不错。唯一不支持它的“主要”浏览器是Firefox(我相信他们很快就会),以及IE(谁关心它)。

免责声明:
我不是开发人员。我只是在处理个人项目时解决JS中不存在的枚举的限制而将其组合在一起。

示例类

class Colors {
    // Private Fields
    static #_RED = 0;
    static #_GREEN = 1;
    static #_BLUE = 2;

    // Accessors for "get" functions only (no "set" functions)
    static get RED() { return this.#_RED; }
    static get GREEN() { return this.#_GREEN; }
    static get BLUE() { return this.#_BLUE; }
}

现在你应该能够直接调用你的枚举。

Colors.RED; // 0
Colors.GREEN; // 1
Colors.BLUE; // 2

使用私有字段和受限制的访问器相结合,意味着现有的枚举值得到了良好的保护(它们现在基本上是常量)。

Colors.RED = 10 // Colors.RED is still 0
Colors._RED = 10 // Colors.RED is still 0
Colors.#_RED = 10 // Colors.RED is still 0

更新:请首先尝试使用TypeScript。如果你必须使用原生JS,那就去吧! - dsanchez

14

这是我的个人方法。

class ColorType {
    static get RED () {
        return "red";
    }

    static get GREEN () {
        return "green";
    }

    static get BLUE () {
        return "blue";
    }
}

// Use case.
const color = Color.create(ColorType.RED);

3
我不建议使用这个,因为它没有提供遍历所有可能值的方法,也没有检查一个值是否为ColorType的方法,除非手动逐一检查。 - Domino
1
我担心这段代码过于冗长,定义枚举类型应该非常简洁。 - Zhe

12

如上所述,您还可以编写一个makeEnum()辅助函数:

function makeEnum(arr){
    let obj = Object.create(null);
    for (let val of arr){
        obj[val] = Symbol(val);
    }
    return Object.freeze(obj);
}

使用方法:

const Colors = makeEnum(["red","green","blue"]);
let startColor = Colors.red; 
console.log(startColor); // Symbol(red)

if(startColor == Colors.red){
    console.log("Do red things");
}else{
    console.log("Do non-red things");
}

2
作为一行代码:const makeEnum = (...lst) => Object.freeze(Object.assign({}, ...lst.map(k => ({[k]: Symbol(k)}))));然后使用它:const colors = makeEnum("Red", "Green", "Blue") - Manuel Ebert
1
我会做一个微小的更改:let obj = Object.create(null),以使对象没有任何属性。 - some
好的建议 - 已经进行了更改。 - tonethar

9

7

可以参考TypeScript的实现。基本上,他们是这样做的:

const MAP = {};

MAP[MAP[1] = 'A'] = 1;
MAP[MAP[2] = 'B'] = 2;

MAP['A'] // 1
MAP[1] // A

使用符号、冻结对象,任何你想要的方式。

我不明白为什么要使用MAP[MAP[1] = 'A'] = 1; 而不是 MAP[1] = 'A'; MAP['A'] = 1;。我一直听说,在表达式中使用赋值语句是个糟糕的风格。此外,从镜像式的赋值中你能得到什么好处呢? - cypherfunc
1
这里是一个链接,介绍了枚举映射是如何在ES5中编译的。https://www.typescriptlang.org/docs/handbook/enums.html#reverse-mappings我能想象将其编译成单行的方式会更加简单和精确,例如MAP[MAP[1] = 'A'] = 1; - givehug
哦,看起来镜像只是使在字符串和数字/符号表示之间轻松切换每个值,并通过执行Enum[Enum[x]] === x检查某些字符串或数字/符号x是否为有效的枚举值。它并没有解决我的原始问题,但可能会有用,并且不会破坏任何东西。 - cypherfunc
1
请记住,TypeScript 添加了一层健壮性,一旦 TS 代码被编译,这种健壮性就会丢失。如果您的整个应用程序都是用 TS 编写的,那很好,但如果您想让 JS 代码更加健壮,冻结符号映射似乎是一种更安全的模式。 - Domino

5
你可以查看 Enumify,这是一个非常好的、功能齐全的 ES6 枚举库。 链接

3
这是我在JavaScript中实现Java枚举的代码。
我还包含了单元测试。

const main = () => {
  mocha.setup('bdd')
  chai.should()

  describe('Test Color [From Array]', function() {
    let Color = new Enum('RED', 'BLUE', 'GREEN')
    
    it('Test: Color.values()', () => {
      Color.values().length.should.equal(3)
    })

    it('Test: Color.RED', () => {
      chai.assert.isNotNull(Color.RED)
    })

    it('Test: Color.BLUE', () => {
      chai.assert.isNotNull(Color.BLUE)
    })

    it('Test: Color.GREEN', () => {
      chai.assert.isNotNull(Color.GREEN)
    })

    it('Test: Color.YELLOW', () => {
      chai.assert.isUndefined(Color.YELLOW)
    })
  })

  describe('Test Color [From Object]', function() {
    let Color = new Enum({
      RED   : { hex: '#F00' },
      BLUE  : { hex: '#0F0' },
      GREEN : { hex: '#00F' }
    })

    it('Test: Color.values()', () => {
      Color.values().length.should.equal(3)
    })

    it('Test: Color.RED', () => {
      let red = Color.RED
      chai.assert.isNotNull(red)
      red.getHex().should.equal('#F00')
    })

    it('Test: Color.BLUE', () => {
      let blue = Color.BLUE
      chai.assert.isNotNull(blue)
      blue.getHex().should.equal('#0F0')
    })

    it('Test: Color.GREEN', () => {
      let green = Color.GREEN
      chai.assert.isNotNull(green)
      green.getHex().should.equal('#00F')
    })

    it('Test: Color.YELLOW', () => {
      let yellow = Color.YELLOW
      chai.assert.isUndefined(yellow)
    })
  })

  mocha.run()
}

class Enum {
  constructor(values) {
    this.__values = []
    let isObject = arguments.length === 1
    let args = isObject ? Object.keys(values) : [...arguments]
    args.forEach((name, index) => {
      this.__createValue(name, isObject ? values[name] : null, index)
    })
    Object.freeze(this)
  }

  values() {
    return this.__values
  }

  /* @private */
  __createValue(name, props, index) {
    let value = new Object()
    value.__defineGetter__('name', function() {
      return Symbol(name)
    })
    value.__defineGetter__('ordinal', function() {
      return index
    })
    if (props) {
      Object.keys(props).forEach(prop => {
        value.__defineGetter__(prop, function() {
          return props[prop]
        })
        value.__proto__['get' + this.__capitalize(prop)] = function() {
          return this[prop]
        }
      })
    }
    Object.defineProperty(this, name, {
      value: Object.freeze(value),
      writable: false
    })
    this.__values.push(this[name])
  }

  /* @private */
  __capitalize(str) {
    return str.charAt(0).toUpperCase() + str.slice(1)
  }
}

main()
.as-console-wrapper { top: 0; max-height: 100% !important; }
<link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/mocha/2.2.5/mocha.css">
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/mocha/2.2.5/mocha.js"></script>
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/chai/3.2.0/chai.js"></script>
<!--

public enum Color {
  RED("#F00"),
  BLUE("#0F0"),
  GREEN("#00F");
  
  private String hex;
  public String getHex()  { return this.hex;  }
  
  private Color(String hex) {
    this.hex = hex;
  }
}

-->
<div id="mocha"></div>


更新

这里是满足MDN要求的更加最新版本。

Object.prototype.__defineGetter__已被Object.defineProperty替代,根据MDN的建议:

该特性已过时,应使用对象初始化语法或 Object.defineProperty() API 定义getter。虽然此特性广泛实现,但由于遗留用途而仅在ECMAScript规范中描述。不应使用此方法,因为存在更好的替代方法。

编辑:添加了枚举值的原型(Enum.__prototype)来处理属性的JSON序列化。

const main = () => {
  mocha.setup('bdd')
  chai.should()

  describe('Test Color [From Array]', function() {
    let Color = new Enum('RED', 'BLUE', 'GREEN')

    it('Test: Color.values()', () => {
      Color.values().length.should.equal(3)
    })

    it('Test: Color.RED', () => {
      chai.assert.isNotNull(Color.RED)
    })

    it('Test: Color.BLUE', () => {
      chai.assert.isNotNull(Color.BLUE)
    })

    it('Test: Color.GREEN', () => {
      chai.assert.isNotNull(Color.GREEN)
    })

    it('Test: Color.YELLOW', () => {
      chai.assert.isUndefined(Color.YELLOW)
    })
  })

  describe('Test Color [From Object]', function() {
    let Color = new Enum({
      RED:   { hex: '#F00' },
      BLUE:  { hex: '#0F0' },
      GREEN: { hex: '#00F' }
    })
    
    it('Test: Color.values()', () => {
      Color.values().length.should.equal(3)
    })

    it('Test: Color.RED', () => {
      let red = Color.RED
      chai.assert.isNotNull(red)
      red.getHex().should.equal('#F00')
      JSON.stringify(red).should.equal('{"hex":"#F00"}')
    })

    it('Test: Color.BLUE', () => {
      let blue = Color.BLUE
      chai.assert.isNotNull(blue)
      blue.getHex().should.equal('#0F0')
      JSON.stringify(blue).should.equal('{"hex":"#0F0"}')
    })

    it('Test: Color.GREEN', () => {
      let green = Color.GREEN
      chai.assert.isNotNull(green)
      green.getHex().should.equal('#00F')
      JSON.stringify(green).should.equal('{"hex":"#00F"}')
    })

    it('Test: Color.YELLOW', () => {
      let yellow = Color.YELLOW
      chai.assert.isUndefined(yellow)
    })
  })

  mocha.run()
}

class Enum {
  constructor(...values) {
    this.__values = []

    const [first, ...rest] = values
    const hasOne = rest.length === 0
    const isArray = Array.isArray(first)
    const args = hasOne ? (isArray ? first : Object.keys(first)) : values

    args.forEach((name, index) => {
      this.__createValue({
        name,
        index,
        props: hasOne && !isArray ? first[name] : null
      })
    })

    Object.freeze(this)
  }

  /* @public */
  values() {
    return this.__values
  }

  /* @private */
  __createValue({ name, index, props }) {
    const value = Object.create(Enum.__prototype(props))

    Object.defineProperties(value, Enum.__defineReservedProps({
      name,
      index
    }))

    if (props) {
      Object.defineProperties(value, Enum.__defineAccessors(props))
    }

    Object.defineProperty(this, name, {
      value: Object.freeze(value),
      writable: false
    })

    this.__values.push(this[name])
  }
}

Enum.__prototype = (props) => ({
  toJSON() {
    return props;
  },
  toString() {
    return JSON.stringify(props);
  }
});

/* @private */
Enum.__defineReservedProps = ({ name, index }) => ({
  name: {
    value: Symbol(name),
    writable: false
  },
  ordinal: {
    value: index,
    writable: false
  }
})

/* @private */
Enum.__defineAccessors = (props) =>
  Object.entries(props).reduce((acc, [prop, val]) => ({
    ...acc,
    [prop]: {
      value: val,
      writable: false
    },
    [`get${Enum.__capitalize(prop)}`]: {
      get: () => function() {
        return this[prop]
      }
    }
  }), {})

/* @private */
Enum.__capitalize = (str) => str.charAt(0).toUpperCase() + str.slice(1)

main()
.as-console-wrapper { top: 0; max-height: 100% !important; }
<link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/mocha/2.2.5/mocha.css">
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/mocha/2.2.5/mocha.js"></script>
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/chai/3.2.0/chai.js"></script>
<!--

public enum Color {
  RED("#F00"),
  BLUE("#0F0"),
  GREEN("#00F");
  
  private String hex;
  public String getHex()  { return this.hex;  }
  
  private Color(String hex) {
    this.hex = hex;
  }
}

-->
<div id="mocha"></div>


2
也许这个解决方案可以帮到您? :)
function createEnum (array) {
  return Object.freeze(array
    .reduce((obj, item) => {
      if (typeof item === 'string') {
        obj[item.toUpperCase()] = Symbol(item)
      }
      return obj
    }, {}))
}

例子:

createEnum(['red', 'green', 'blue']);

> {RED: Symbol(red), GREEN: Symbol(green), BLUE: Symbol(blue)}

以下是关于编程的内容,请将其从英语翻译成中文。只返回翻译后的文本:非常感谢提供一个用法示例 :-) - Abderrahmane TAHRI JOUTI

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接