【问题标题】:Enums in Javascript with ES6ES6 中的 Javascript 枚举
【发布时间】:2017-11-10 21:13:48
【问题描述】:

我正在用 Javascript 重建一个旧的 Java 项目,并意识到在 JS 中没有很好的方法来做枚举。

我能想到的最好的是:

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

const 使 Colors 不会被重新分配,并冻结它可以防止键和值发生变化。我正在使用符号,以便 Colors.RED 不等于 0,或除自身之外的任何其他内容。

这个公式有问题吗?有没有更好的方法?


(我知道这个问题有点重复,但是所有的previous Q/As 都相当老了,ES6 为我们提供了一些新功能。)


编辑:

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

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

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

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

【问题讨论】:

  • 这将是 es6 中的完美方法。你不必冻结它
  • @Nirus,如果你不想修改它,你可以这样做。
  • 你注意到this answer了吗?
  • 我能想到的一个问题:不能将此枚举与JSON.stringify() 一起使用。无法序列化/反序列化Symbol
  • @ErictheRed 我多年来一直在使用字符串枚举常量值,没有任何麻烦,因为使用 Flow(或 TypeScript)比担心避免碰撞更能保证类型安全

标签: javascript enums ecmascript-6 immutability symbols


【解决方案1】:

这个公式有问题吗?

我没有看到。

有没有更好的办法?

我会将这两个语句合并为一个:

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

如果您不喜欢样板文件,例如重复的 Symbol 调用,您当然也可以编写一个辅助函数 makeEnum,从名称列表中创建相同的内容。

【讨论】:

  • 这里没有领域问题吗?
  • @torazaburo 你的意思是,当代码被加载两次时,它会生成不同的符号,这不会是字符串的问题吗?是的,好点子,让它成为一个答案:-)
  • @ErictheRed 不,Symbol.for 确实没有 有跨领域问题,但是它确实存在与truly global namespace 的常见冲突问题。
  • @ErictheRed 它确实保证创建完全相同的符号,无论何时何地(从哪个领域/框架/选项卡/进程)调用它
  • @Sky Colors 中查找的默认值与枚举定义无关。可以像往常一样使用Colors[name] || Colors.BLUEColors.hasOwnProperty(name) ? Colors[name] : Colors.BLUE
【解决方案2】:

虽然使用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 enum annotation 记录的枚举属性和类型。

Colors.RED === Colors.REDtrueColors.RED === Colors.BLUEfalse 时,平等可以正常工作。

【讨论】:

    【解决方案3】:

    这是我个人的做法。

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

    【讨论】:

    • 我不建议使用它,因为它无法迭代所有可能的值,也无法在不手动检查每个值的情况下检查值是否为 ColorType。
    • 这个Enum类型的定义恐怕代码太多了,应该很简洁
    【解决方案4】:

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

    function makeEnum(arr){
        let obj = {};
        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");
    }
    

    【讨论】:

    • 作为单行:const makeEnum = (...lst) => Object.freeze(Object.assign({}, ...lst.map(k => ({[k]: Symbol(k)})))); 然后作为const colors = makeEnum("Red", "Green", "Blue")使用
    【解决方案5】:

    如果你不需要 ES6 并且可以使用 Typescript,它有一个不错的enum

    https://www.typescriptlang.org/docs/handbook/enums.html

    【讨论】:

      【解决方案6】:

      检查how TypeScript does it。 基本上他们会做以下事情:

      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;。我一直听说使用赋值作为表达式是不好的风格。另外,你从镜像分配中得到什么好处?
      • 这里是他们的文档中如何将枚举映射编译为 es5 的链接。 typescriptlang.org/docs/handbook/enums.html#reverse-mappings 我可以想象将其编译为单行代码会更简单、更简洁,例如MAP[MAP[1] = 'A'] = 1;
      • 嗯。所以看起来镜像只是使每个值的字符串和数字/符号表示之间切换变得容易,并通过执行Enum[Enum[x]] === x 检查某些字符串或数字/符号x 是否是有效的枚举值。它不能解决我最初的任何问题,但可能很有用并且不会破坏任何东西。
      • 请记住,TypeScript 增加了一层健壮性,一旦编译 TS 代码就会丢失。如果您的整个应用程序都是用 TS 编写的,那就太好了,但如果您希望 JS 代码健壮,那么符号的冻结映射听起来像是一种更安全的模式。
      【解决方案7】:

      2020 年 5 月 11 日更新:
      修改为包含静态字段和方法以更接近地复制“真实”枚举行为。

      如果您计划更新,我建议您尝试使用我所说的“枚举类”(除非您不能接受任何浏览器或运行时环境限制)。它基本上是一个非常简单且干净的类,它使用私有字段和有限的访问器来模拟枚举的行为。当我想在枚举中构建更多功能时,我有时会在 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
      

      【讨论】:

        【解决方案8】:

        您可以查看Enumify,这是一个非常优秀且功能强大的 ES6 枚举库。

        【讨论】:

          【解决方案9】:

          这是我在 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 的更新版本。

          根据 MDN 的建议,Object.prototype.__defineGetter__ 已替换为 Object.defineProperty

          此功能已被弃用,取而代之的是使用对象初始值设定项语法或 Object.defineProperty() API 定义 getter。虽然此功能已被广泛实施,但由于遗留使用,它仅在 ECMAScript 规范中进行了描述。不应使用此方法,因为存在更好的替代方法。

          编辑:为枚举值添加了一个原型 (Enum.__prototype) 以处理 props 的 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>

          【讨论】:

            【解决方案10】:

            也许这个解决方案? :)

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

            【讨论】:

            • 一个使用示例将不胜感激:-)
            【解决方案11】:

            您也可以使用 es6-enum 包 (https://www.npmjs.com/package/es6-enum)。它非常易于使用。请参见下面的示例:

            import Enum from "es6-enum";
            const Colors = Enum("red", "blue", "green");
            Colors.red; // Symbol(red)
            

            【讨论】:

              【解决方案12】:

              这是一个 Enum 工厂,它通过使用命名空间和 Symbol.for 来避免领域问题:

              const Enum = (n, ...v) => Object.freeze(v.reduce((o, v) => (o[v] = Symbol.for(`${n}.${v}`), o), {}));
              
              const COLOR = Enum("ACME.Color", "Blue", "Red");
              console.log(COLOR.Red.toString());
              console.log(COLOR.Red === Symbol.for("ACME.Color.Red"));

              【讨论】:

                【解决方案13】:

                我更喜欢 @tonethar 的方法,它进行了一些改进和挖掘,以便更好地理解 ES6/Node.js 生态系统的底层。以服务器端的背景为背景,我更喜欢围绕平台原语的功能风格的方法,这最大限度地减少了代码膨胀,由于引入新类型和增加而导致死亡阴影的滑坡进入状态的管理谷可读性 - 使解决方案和算法的意图更加清晰。

                解决方案TDDES6Node.jsLodashJestBabelESLint

                // ./utils.js
                import _ from 'lodash';
                
                const enumOf = (...args) =>
                  Object.freeze( Array.from( Object.assign(args) )
                    .filter( (item) => _.isString(item))
                    .map((item) => Object.freeze(Symbol.for(item))));
                
                const sum = (a, b) => a + b;
                
                export {enumOf, sum};
                // ./utils.js
                
                // ./kittens.js
                import {enumOf} from "./utils";
                
                const kittens = (()=> {
                  const Kittens = enumOf(null, undefined, 'max', 'joe', 13, -13, 'tabby', new 
                    Date(), 'tom');
                  return () => Kittens;
                })();
                
                export default kittens();
                // ./kittens.js 
                
                // ./utils.test.js
                import _ from 'lodash';
                import kittens from './kittens';
                
                test('enum works as expected', () => {
                  kittens.forEach((kitten) => {
                    // in a typed world, do your type checks...
                    expect(_.isSymbol(kitten));
                
                    // no extraction of the wrapped string here ...
                    // toString is bound to the receiver's type
                    expect(kitten.toString().startsWith('Symbol(')).not.toBe(false);
                    expect(String(kitten).startsWith('Symbol(')).not.toBe(false);
                    expect(_.isFunction(Object.valueOf(kitten))).not.toBe(false);
                
                    const petGift = 0 === Math.random() % 2 ? kitten.description : 
                      Symbol.keyFor(kitten);
                    expect(petGift.startsWith('Symbol(')).not.toBe(true);
                    console.log(`Unwrapped Christmas kitten pet gift '${petGift}', yeee :) 
                    !!!`);
                    expect(()=> {kitten.description = 'fff';}).toThrow();
                  });
                });
                // ./utils.test.js
                

                【讨论】:

                • Array.from(Object.assign(args)) 绝对什么都不做。你可以直接使用...args
                【解决方案14】:

                这是我的方法,包括一些辅助方法

                export default class Enum {
                
                    constructor(name){
                        this.name = name;
                    }
                
                    static get values(){
                        return Object.values(this);
                    }
                
                    static forName(name){
                        for(var enumValue of this.values){
                            if(enumValue.name === name){
                                return enumValue;
                            }
                        }
                        throw new Error('Unknown value "' + name + '"');
                    }
                
                    toString(){
                        return this.name;
                    }
                }
                

                -

                import Enum from './enum.js';
                
                export default class ColumnType extends Enum {  
                
                    constructor(name, clazz){
                        super(name);        
                        this.associatedClass = clazz;
                    }
                }
                
                ColumnType.Integer = new ColumnType('Integer', Number);
                ColumnType.Double = new ColumnType('Double', Number);
                ColumnType.String = new ColumnType('String', String);
                

                【讨论】:

                  【解决方案15】:

                  我使用了这种模仿 C# 和 Java 枚举的方法。请注意EnumFunction 的子类,因此枚举值是对象,您可以将实例方法添加到枚举中。

                  function inherits(ctor, superCtor) {
                      ctor.super_ = superCtor
                      Object.setPrototypeOf(ctor.prototype, superCtor.prototype)
                  }
                  
                  function Enum(...values) {
                      // refer to `T` instead of `this`
                      function T(id) {
                          if(id >= values.length || id < 0) {
                             throw new Error("invalid enum id");
                          }
                  
                          this.id = id;
                      }
                  
                     T.prototype.toString = function () { return values[this.id]; }
                      
                      
                      Object.setPrototypeOf(T, Enum.prototype);
                      T._values = values;
                      for(var i = 0; i < values.length; i++) {  
                         var enum_value = new T(i);
                          T[i] = enum_value; //allows for reverse lookup
                          T[values[i]] = enum_value;
                      }
                      
                      return T;
                  }
                  
                  Enum.prototype.get_values = function () {
                      return this._values;
                  }
                  
                  
                  inherits(Enum, Function);
                  
                  //Test
                  var Color = new Enum("RED", "GREEN", "BLUE");
                  
                  //Color is an actual class, so you can add methods to it (but not new enum values).
                  Color.prototype.something = function () { return 100; }
                  
                  
                  console.log(Color.RED);
                  console.log(Color.RED instanceof Color);
                  console.log(Color.RED.something());
                  console.log(Color.get_values());
                    

                  通过使用类实例作为值,您可以获得与符号相同的碰撞避免。

                  【讨论】:

                    【解决方案16】:
                    const Colors = (function(Colors) {
                      Colors[Colors["RED"] = "#f00"] = "RED";
                      return Object.freeze(Colors);
                    })({});
                    Colors.RED = "#000" // <= Will fail because object is frozen
                    console.log(Colors.RED); // #f00
                    console.log(Colors['#f00']); // RED
                    

                    【讨论】:

                      【解决方案17】:

                      你可以使用 ES6 Map

                      const colors = new Map([
                        ['RED', 'red'],
                        ['BLUE', 'blue'],
                        ['GREEN', 'green']
                      ]);
                      
                      console.log(colors.get('RED'));
                      

                      【讨论】:

                      • 恕我直言,这是一个糟糕的解决方案,因为它的复杂性(每次都应该调用访问器方法)和枚举性质的矛盾(可以调用 mutator 方法并更改任何键的值)......所以使用const x = Object.freeze({key: 'value'}) 来获得看起来和行为类似于 ES6 中的枚举的东西
                      • 您必须传递一个字符串才能获取该值,就像您所做的 colors.get('RED') 一样。这很容易出错。
                      猜你喜欢
                      • 2017-08-17
                      • 2011-03-22
                      • 2010-12-10
                      • 1970-01-01
                      • 1970-01-01
                      • 2016-04-03
                      • 1970-01-01
                      • 2015-08-08
                      • 2010-11-26
                      相关资源
                      最近更新 更多