ES6中的Java枚举


136

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

我能想到的最好的是:

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

const保持Colors被重新分配,并冷冻它防止突变的键和值。我正在使用Symbols,所以它Colors.RED不等于0或除自身以外的其他任何符号。

这种配方有问题吗?有没有更好的办法?


(我知道这个问题有点重复,但是以前的所有Q / A都已经很老了,ES6为我们提供了一些新功能。)


编辑:

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

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

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

通过使用对象引用作为值,您将获得与Symbols相同的避免碰撞的功能。


2
这将是es6中的理想方法。您不必冻结它
Nirus

2
如果您不希望修改@Nirus,可以这样做。
zerkms

2
您注意到这个答案了吗?
Bergi

3
我想到的一个问题:不能与一起使用此枚举JSON.stringify()。无法序列化/反序列化Symbol
le_m

1
@ErictheRed我多年来一直在使用字符串枚举常量值,而没有任何麻烦,因为使用Flow(或TypeScript)可以保证类型安全性比担心碰撞避免的方式更安全
Andy

Answers:


131

这种配方有问题吗?

我没看到。

有没有更好的办法?

我将两个语句合而为一:

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

如果您不喜欢样板文件(如重复Symbol调用),那么当然也可以编写一个帮助程序函数makeEnum,该函数从名称列表中创建相同的东西。


3
这里没有领域问题吗?

2
@torazaburo您的意思是,当代码两次加载时,它将生成不同的符号,这对字符串来说不是问题吗?是的,很好,请回答:-)
Bergi

2
@ErictheRed不, Symbol.for具有跨领域的问题,但它确实有一个通常的碰撞问题,真正的全局命名空间
Bergi

1
@ErictheRed它确实保证无论何时何地(从哪个领域/框架/选项卡/进程)被调用都创建完全相同的符号
Bergi

1
@jamesemanon如果需要,可以获取描述,,但我主要将其用于调试。而是像往常一样(沿线内容enum => ({[Colors.RED]: "bright red", [Colors.BLUE]: "deep blue", [Colors.GREEN]: "grass green"}[enum]))具有自定义的枚举到字符串转换功能。
贝尔吉'18

18

虽然将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.RED存在trueColors.RED === Colors.BLUE存在一样可以预期地工作false


9

如上所述,您还可以编写一个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");
}

2
作为单线: const makeEnum = (...lst) => Object.freeze(Object.assign({}, ...lst.map(k => ({[k]: Symbol(k)})))); 然后将其用作 const colors = makeEnum("Red", "Green", "Blue")
Manuel Ebert

9

这是我的个人方法。

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。
Domino

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;。我一直听说使用赋值作为表达式是不好的风格。此外,您从镜像分配中获得什么好处?
红色的埃里克(Eric the Red)

1
这是在他们的文档中如何将枚举映射编译为es5的链接。typescriptlang.org/docs/handbook/enums.html#reverse-mappings可以想象将它编译成单行代码会更加容易和简洁MAP[MAP[1] = 'A'] = 1;
Givehug

嗯 因此,看起来就像通过镜像可以轻松地在每个值的字符串和数字/符号表示形式之间切换,并通过执行来检查某个字符串或数字/符号x是否为有效的Enum值Enum[Enum[x]] === x。它不能解决我的任何原始问题,但是可能有用并且不会破坏任何内容。
Eric the Red

1
请记住,TypeScript增加了一层健壮性,一旦编译TS代码,该健壮性就会丢失。如果您的整个应用程序都是用TS编写的,那太好了,但是如果您希望JS代码更健壮,则冻结的符号映射听起来像是一种更安全的模式。
Domino



1

也许这个解决方案?:)

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

0

我更喜欢@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
Domino

0

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

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


0

这是我在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>


-3

您可以使用ES6地图

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

console.log(colors.get('RED'));

恕我直言,这是一个糟糕的解决方案,因为它很复杂(每次都应调用accessor方法)和对枚举性质的ontradiction(可以调用mutator方法并更改任何键的值)...因此请const x = Object.freeze({key: 'value'})改用它来获取看起来和表现像ES6中的枚举
Yurii Rabeshko 18/09/22

您必须像传递colors.get('RED')一样传递字符串以获取值。容易出错。
阿德里安奥维耶多
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.