Enums en Javascript con ES6


136

Estoy reconstruyendo un antiguo proyecto Java en Javascript, y me di cuenta de que no hay una buena manera de hacer enumeraciones en JS.

Lo mejor que se me ocurre es:

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

El constguarda Colorsde ser reasignado, y la congelación impide que la mutación de las claves y valores. Estoy usando Símbolos para que Colors.REDno sea igual a 0, ni nada más aparte de sí mismo.

¿Hay algún problema con esta formulación? ¿Hay una mejor manera?


(Sé que esta pregunta se repite un poco, pero todas las preguntas y respuestas anteriores son bastante antiguas y ES6 nos brinda algunas capacidades nuevas).


EDITAR:

Otra solución, que trata el problema de la serialización, pero creo que todavía tiene problemas de dominio:

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

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

Al utilizar referencias de objetos como valores, obtiene la misma prevención de colisiones que los símbolos.


2
este sería un enfoque perfecto en es6. No tienes que congelarlo
Nirus

2
@Nirus, si no quieres que se modifique.
zerkms

2
¿Notaste esta respuesta ?
Bergi

3
Un problema que se me ocurre: no se puede usar esta enumeración con JSON.stringify(). No se puede serializar / deserializar Symbol.
le_m

1
@ErictheRed He estado usando valores constantes de enumeración de cadenas durante años sin problemas, porque el uso de Flow (o TypeScript) garantiza una mayor seguridad de tipo que preocuparse por evitar la colisión
Andy

Respuestas:


131

¿Hay algún problema con esta formulación?

No veo ninguno

¿Hay una mejor manera?

Colapsaría las dos declaraciones en una:

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

Si no le gusta la repetitiva, como las Symbolllamadas repetidas , también puede escribir una función auxiliar makeEnumque cree lo mismo a partir de una lista de nombres.


3
¿No hay problemas de reino aquí?

2
@torazaburo ¿Quiere decir que cuando el código se carga dos veces generará diferentes símbolos, lo que no sería un problema con las cadenas? Sí, buen punto, que sea una respuesta :-)
Bergi

2
@ErictheRed No, Symbol.forno , no tienen problemas de cross-reino, sin embargo, tiene el problema de colisión corriente con un espacio de nombres verdaderamente global .
Bergi

1
@ErictheRed De hecho, garantiza crear exactamente el mismo símbolo independientemente de cuándo y dónde (desde qué reino / marco / pestaña / proceso) se llama
Bergi

1
@jamesemanon Puede obtener la descripción si lo desea , pero la usaría principalmente para depurar solamente. Por el contrario, tenga una función de conversión personalizada de enum a cadena como de costumbre (algo similar enum => ({[Colors.RED]: "bright red", [Colors.BLUE]: "deep blue", [Colors.GREEN]: "grass green"}[enum])).
Bergi

18

Si bien el uso Symbolcomo el valor enum funciona bien para casos de uso simples, puede ser útil dar propiedades a las enumeraciones. Esto se puede hacer usando un Objectcomo el valor de enumeración que contiene las propiedades.

Por ejemplo, podemos dar a cada uno Colorsun nombre y un 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" }
});

La inclusión de propiedades en la enumeración evita tener que escribir switchdeclaraciones (y posiblemente olvidar nuevos casos en las instrucciones de cambio cuando se extiende una enumeración). El ejemplo también muestra las propiedades y tipos de enumeración documentados con la anotación de enumeración JSDoc .

La igualdad funciona como se espera con Colors.RED === Colors.REDser truey Colors.RED === Colors.BLUEser false.


9

Como se mencionó anteriormente, también podría escribir una makeEnum()función auxiliar:

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

Úselo así:

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 una frase: const makeEnum = (...lst) => Object.freeze(Object.assign({}, ...lst.map(k => ({[k]: Symbol(k)})))); luego úsela como const colors = makeEnum("Red", "Green", "Blue")
Manuel Ebert

9

Este es mi enfoque personal.

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

No recomendaría usar esto ya que no proporciona una forma de iterar sobre todos los valores posibles, y no hay forma de verificar si un valor es un ColorType sin verificar manualmente cada uno.
Domino

7

Comprueba cómo TypeScript lo hace . Básicamente hacen lo siguiente:

const MAP = {};

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

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

Usa símbolos, congela objetos, lo que quieras.


No estoy siguiendo por qué usa en MAP[MAP[1] = 'A'] = 1;lugar de MAP[1] = 'A'; MAP['A'] = 1;. Siempre he escuchado que usar una tarea como expresión es un mal estilo. Además, ¿qué beneficio obtiene de las tareas reflejadas?
Eric the Red

1
Aquí hay un enlace a cómo se compila la asignación de enumeración a es5 en sus documentos. typescriptlang.org/docs/handbook/enums.html#reverse-mappings Puedo imaginar que sería simplemente más fácil y conciso compilarlo en una sola línea, por ejemplo MAP[MAP[1] = 'A'] = 1;.
Givehug

Huh Por lo tanto, parece que la duplicación solo facilita el cambio entre la cadena y las representaciones de números / símbolos de cada valor, y verifica que alguna cadena o número / símbolo xsea ​​un valor Enum válido al hacerlo Enum[Enum[x]] === x. No resuelve ninguno de mis problemas originales, pero podría ser útil y no rompe nada.
Eric the Red

1
Tenga en cuenta que TypeScript agrega una capa de robustez que se pierde una vez que se compila el código TS. Si toda su aplicación está escrita en TS, es genial, pero si desea que el código JS sea robusto, el mapa congelado de símbolos suena como un patrón más seguro.
Domino



1

Tal vez esta solución? :)

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

Ejemplo:

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

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

un ejemplo de uso sería muy apreciado :-)
Abderrahmane TAHRI JOUTI

0

Prefiero el enfoque de @ tonethar, con un poco de mejoras y excavaciones para el beneficio de comprender mejor los fundamentos del ecosistema ES6 / Node.js. Con un fondo en el lado del servidor de la cerca, prefiero el enfoque del estilo funcional alrededor de las primitivas de la plataforma, esto minimiza la hinchazón del código, la pendiente resbaladiza en el valle de administración de la sombra de la muerte del estado debido a la introducción de nuevos tipos y aumentos la legibilidad: deja más clara la intención de la solución y el algoritmo.

Solución con 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))no hace absolutamente nada Podrías usar ...argsdirectamente.
Domino

0

Aquí está mi enfoque, incluidos algunos métodos de ayuda

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

Aquí está mi implementación de una enumeración de Java en JavaScript.

También incluí pruebas unitarias.

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

Podrías usar ES6 Map

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

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

En mi humilde opinión, es una mala solución debido a su complejidad (debe llamar al método de acceso cada vez) y la prohibición de la naturaleza de enumeración (puede llamar al método del mutador y cambiar el valor de cualquier clave) ... así que use const x = Object.freeze({key: 'value'})en su lugar para obtener algo que se vea y se comporta como enum en ES6
Yurii Rabeshko

Debe pasar una cadena para obtener el valor, como hizo con colors.get ('RED'). Que es propenso a errores.
adrian oviedo
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.