Enums em Javascript com ES6


136

Estou reconstruindo um projeto Java antigo em Javascript e percebi que não há uma boa maneira de criar enumerações em JS.

O melhor que posso apresentar é:

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

Os constmantimentos Colorsnão são reatribuídos e o congelamento evita que as chaves e os valores sejam alterados. Estou usando símbolos para que Colors.REDnão seja igual a 0, ou qualquer outra coisa além de si.

Existe algum problema com esta formulação? Existe uma maneira melhor?


(Eu sei que esta pergunta é um pouco repetida, mas todas as perguntas / respostas anteriores são bastante antigas e o ES6 nos oferece alguns novos recursos.)


EDITAR:

Outra solução, que lida com o problema de serialização, mas acredito que ainda tem problemas de domínio:

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

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

Ao usar referências de objeto como valores, você obtém a mesma prevenção de colisão que Símbolos.


2
essa seria uma abordagem perfeita no es6. Você não precisa congelá-lo
Nirus

2
@ Virus que você faz, se não quiser que seja modificado.
Zerkms 9/06

2
Você notou esta resposta ?
Bergi 09/07

3
Uma questão em que consigo pensar: Não é possível usar esta enumeração JSON.stringify(). Não é possível serializar / desserializarSymbol .
le_m

1
@ErictheRed Estou usando valores constantes de enum de string há anos sem aborrecimentos, porque usar Flow (ou TypeScript) garante muito mais segurança de tipo do que se preocupar com a prevenção de colisões
Andy

Respostas:


131

Existe algum problema com esta formulação?

Eu não vejo nenhum.

Existe uma maneira melhor?

Eu recolheria as duas instruções em uma:

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

Se você não gosta do clichê, como as Symbolchamadas repetidas , é claro que também pode escrever uma função auxiliar makeEnumque cria a mesma coisa a partir de uma lista de nomes.


3
Não há problemas de domínio aqui?

2
@torazaburo Quer dizer, quando o código é carregado duas vezes, ele gera símbolos diferentes, o que não seria um problema para as strings? Sim, bom ponto, torná-lo uma resposta :-)
Bergi

2
@ErictheRed Não, Symbol.fornão não tem problemas de cross-realm, no entanto, tem o problema de costume colisão com um namespace verdadeiramente global .
Bergi

1
@ErictheRed este garanta efectivamente para criar o mesmo símbolo exato, independentemente de quando e onde (a partir do qual reino / frame / guia / processo) é chamado
Bergi

1
@jamesemanon Você pode obter a descrição se quiser , mas eu a usaria principalmente apenas para depuração. Em vez disso, tenha uma função de conversão personalizada de enum para string, como de costume (algo parecido enum => ({[Colors.RED]: "bright red", [Colors.BLUE]: "deep blue", [Colors.GREEN]: "grass green"}[enum])).
Bergi

18

Embora o uso Symbolcomo valor de enum funcione bem em casos de uso simples, pode ser útil fornecer propriedades para enumerações. Isso pode ser feito usando umObject valor como enum que contém as propriedades.

Por exemplo, podemos atribuir a cada Colorsum um nome e valor hexadecimal:

/**
 * 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" }
});

A inclusão de propriedades no enum evita a necessidade de escrever switchinstruções (e possivelmente esquece novos casos nas instruções switch quando um enum é estendido). O exemplo também mostra as propriedades e tipos de enumeração documentados com a anotação de enumeração JSDoc .

A igualdade funciona como esperado com o Colors.RED === Colors.REDser truee o Colors.RED === Colors.BLUEser false.


9

Como mencionado acima, você também pode escrever uma makeEnum()função auxiliar:

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

Use-o assim:

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
Como one-liner: const makeEnum = (...lst) => Object.freeze(Object.assign({}, ...lst.map(k => ({[k]: Symbol(k)})))); Em seguida, use-o como const colors = makeEnum("Red", "Green", "Blue")
Manuel Ebert

9

Esta é a minha abordagem pessoal.

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

Eu não recomendaria usá-lo, pois ele não oferece nenhuma maneira de iterar sobre todos os valores possíveis, nem uma maneira de verificar se um valor é um ColorType sem verificar manualmente cada um.
Domino

7

Veja como o TypeScript faz isso . Basicamente, eles fazem o seguinte:

const MAP = {};

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

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

Use símbolos, congele objetos, o que quiser.


Eu não estou seguindo por que ele usa em MAP[MAP[1] = 'A'] = 1;vez de MAP[1] = 'A'; MAP['A'] = 1;. Eu sempre ouvi dizer que usar uma tarefa como expressão é um estilo ruim. Além disso, que benefício você obtém das atribuições espelhadas?
Eric the Red

1
Aqui está um link para como o mapeamento enum é compilado para es5 em seus documentos. typescriptlang.org/docs/handbook/enums.html#reverse-mappings Posso imaginar que seria simplesmente mais fácil e conciso compilá-lo em uma única linha, por exemplo MAP[MAP[1] = 'A'] = 1;.
21418 darhug

Hã. Portanto, parece que o espelhamento facilita a alternância entre as representações de string e número / símbolo de cada valor e verifique se alguma string ou número / símbolo xé um valor válido de Enum Enum[Enum[x]] === x. Ele não resolve nenhum dos meus problemas originais, mas pode ser útil e não quebra nada.
Eric the Red

1
Lembre-se de que o TypeScript adiciona uma camada de robustez que é perdida quando o código TS é compilado. Se todo o seu aplicativo estiver escrito em TS, é ótimo, mas se você deseja que o código JS seja robusto, o mapa congelado de símbolos soa como um padrão mais seguro.
Domino


4

Você pode verificar o Enumify , uma biblioteca muito boa e com muitos recursos para enumerações do ES6.


1

Talvez esta solução? :)

function createEnum (array) {
  return Object.freeze(array
    .reduce((obj, item) => {
      if (typeof item === 'string') {
        obj[item.toUpperCase()] = Symbol(item)
      }
      return obj
    }, {}))
}

Exemplo:

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

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

um exemplo de uso seria muito apreciado :-)
Abderrahmane TAHRI JOUTI

0

Prefiro a abordagem do @ tonethar, com algumas melhorias e escavações para o benefício de entender melhor os fundamentos do ecossistema ES6 / Node.js. Com um background no lado do servidor da cerca, eu prefiro a abordagem do estilo funcional em torno das primitivas da plataforma, isso minimiza o inchaço do código, a ladeira escorregadia no vale de gerenciamento do estado da sombra da morte devido à introdução de novos tipos e aumentos a legibilidade - deixa mais clara a intenção da solução e o algoritmo.

Solução com TDD , ES6 , Node.js , Lodash , Jest , Babel , ESLint

// ./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))não faz absolutamente nada. Você poderia apenas usar ...argsdiretamente.
Domino

0

Aqui está minha abordagem, incluindo alguns métodos auxiliares

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

Aqui está minha implementação de uma enumeração Java em JavaScript.

Eu também incluí testes de unidade.

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

Você poderia usar o ES6 Map

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

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

IMHO é uma solução ruim por causa de sua complexidade (deve chamar o método acessador toda vez) e contradição da natureza do enum (pode chamar o método mutator e alterar o valor de qualquer chave) ... então use const x = Object.freeze({key: 'value'})para obter algo que pareça e se comporta como enum em ES6
Yurii Rabeshko

Você deve passar uma string para obter o valor, como fez colors.get ('RED'). O que é propenso a erros.
adrian oviedo
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.