Enumera in Javascript con ES6


136

Sto ricostruendo un vecchio progetto Java in Javascript e mi sono reso conto che non esiste un buon modo per fare enumerazioni in JS.

Il meglio che posso inventare è:

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

La constmantiene Colorsdalla riassegnazione e congela impedisce mutando le chiavi e valori. Sto usando i simboli in modo che Colors.REDnon sia uguale 0o altro oltre a se stesso.

C'è un problema con questa formulazione? C'è un modo migliore?


(So ​​che questa domanda è un po 'ripetitiva, ma tutte le precedenti domande e risposte sono piuttosto vecchie e ES6 ci offre alcune nuove funzionalità.)


MODIFICARE:

Un'altra soluzione, che affronta il problema di serializzazione, ma credo che abbia ancora problemi nel regno:

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

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

Utilizzando i riferimenti agli oggetti come valori, si ottiene la stessa evitamento delle collisioni dei simboli.


2
questo sarebbe un approccio perfetto in es6. Non devi congelarlo
Nirus il

2
@Nirus, se non vuoi che venga modificato.
zerkms,

2
Hai notato questa risposta ?
Bergi,

3
Un problema a cui riesco a pensare: non posso usare questo enum con JSON.stringify(). Impossibile serializzare / deserializzare Symbol.
le_m

1
@ErictheRed Sto usando valori costanti di enumerazione stringa da anni senza problemi, perché l'utilizzo di Flow (o TypeScript) garantisce una sicurezza del tipo molto più di quanto non si possa mai preoccupare di evitare le collisioni
Andy,

Risposte:


131

C'è un problema con questa formulazione?

Non ne vedo nessuno.

C'è un modo migliore?

Comprenderei le due affermazioni in una sola:

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

Se non ti piace il boilerplate, come le Symbolchiamate ripetute , puoi ovviamente anche scrivere una funzione di supporto makeEnumche crea la stessa cosa da un elenco di nomi.


3
Non ci sono problemi di regno qui?

2
@torazaburo Intendi, quando il codice viene caricato due volte genererà simboli diversi, che non sarebbe un problema con le stringhe? Sì, buon punto, fagli una risposta :-)
Bergi

2
@ErictheRed No, Symbol.fornon non hanno problemi di cross-realm, tuttavia ha il solito problema di collisione con un namespace veramente globale .
Bergi,

1
@ErictheRed Garantisce infatti la creazione dello stesso identico simbolo indipendentemente da quando e dove (da quale reame / frame / tab / processo) viene chiamato
Bergi,

1
@jamesemanon Puoi ottenere la descrizione se vuoi , ma la userei principalmente per il debug. Piuttosto ha una funzione di conversione enum-to-string personalizzata come al solito (qualcosa lungo le linee enum => ({[Colors.RED]: "bright red", [Colors.BLUE]: "deep blue", [Colors.GREEN]: "grass green"}[enum])).
Bergi,

18

Mentre l'utilizzo Symbolcome valore enum funziona bene per casi d'uso semplici, può essere utile dare proprietà agli enum. Questo può essere fatto usando unObject come valore enum contenente le proprietà.

Ad esempio, possiamo dare a ciascuno Colorsun nome e un valore esadecimale:

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

Includere le proprietà switchnell'enum evita di dover scrivere istruzioni (e possibilmente dimenticare nuovi casi nelle istruzioni switch quando un enum viene esteso). L'esempio mostra anche le proprietà e i tipi di enum documentati con l' annotazione enum JSDoc .

L'uguaglianza funziona come previsto con l' Colors.RED === Colors.REDessere truee l' Colors.RED === Colors.BLUEessere false.


9

Come accennato in precedenza, puoi anche scrivere una makeEnum()funzione di supporto:

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

Usalo in questo modo:

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
Come one-liner: const makeEnum = (...lst) => Object.freeze(Object.assign({}, ...lst.map(k => ({[k]: Symbol(k)})))); quindi usalo come const colors = makeEnum("Red", "Green", "Blue")
Manuel Ebert

9

Questo è il mio approccio personale.

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

Non consiglierei di usarlo in quanto non fornisce alcun modo per scorrere su tutti i valori possibili e nessun modo per verificare se un valore è un ColorType senza verificare manualmente per ciascuno di essi.
Domino

7

Controlla come funziona TypeScript . Fondamentalmente fanno quanto segue:

const MAP = {};

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

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

Usa i simboli, congela l'oggetto, qualunque cosa tu voglia.


Non sto seguendo il motivo per cui utilizza MAP[MAP[1] = 'A'] = 1;invece di MAP[1] = 'A'; MAP['A'] = 1;. Ho sempre sentito che usare un compito come espressione è un cattivo stile. Inoltre, quali vantaggi ottieni dai compiti con mirroring?
Eric the Red,

1
Ecco un link a come viene compilata la mappatura enum in es5 nei loro documenti. typescriptlang.org/docs/handbook/enums.html#reverse-mappings Posso immaginare che sarebbe semplicemente più semplice e più conciso compilarlo su un'unica riga, ad es MAP[MAP[1] = 'A'] = 1;.
dare

Huh. Quindi sembra che il mirroring semplifichi semplicemente il passaggio tra le rappresentazioni stringa e numero / simbolo di ciascun valore, e verificando che una stringa o un numero / simbolo xsia un valore Enum valido Enum[Enum[x]] === x. Non risolve nessuno dei miei problemi originali, ma potrebbe essere utile e non rompe nulla.
Eric the Red,

1
Tenere presente che TypeScript aggiunge un livello di robustezza che viene perso una volta compilato il codice TS. Se la tua intera app è scritta in TS è eccezionale, ma se vuoi che il codice JS sia robusto, la mappa dei simboli congelata suona come un modello più sicuro.
Domino,



1

Forse questa soluzione? :)

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

Esempio:

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

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

un esempio di utilizzo sarebbe molto apprezzato :-)
Abderrahmane TAHRI JOUTI

0

Preferisco l'approccio di @ tonethar, con un po 'di miglioramenti e scavi per il beneficio di comprendere meglio i sottostanti dell'ecosistema ES6 / Node.js. Con uno sfondo sul lato server della recinzione, preferisco l'approccio dello stile funzionale attorno alle primitive della piattaforma, questo minimizza il gonfiore del codice, la pendenza scivolosa nella valle della gestione dello stato dell'ombra della morte a causa dell'introduzione di nuovi tipi e aumenti la leggibilità - rende più chiaro l'intento della soluzione e dell'algoritmo.

Soluzione 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))non fa assolutamente nulla. Potresti semplicemente usare ...argsdirettamente.
Domino

0

Ecco il mio approccio, compresi alcuni metodi di supporto

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

Ecco la mia implementazione di un elenco Java in JavaScript.

Ho anche incluso test unitari.

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

È possibile utilizzare ES6 Map

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

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

IMHO è una cattiva soluzione a causa della sua complessità (dovrebbe chiamare il metodo accessor ogni volta) e della contraddizione della natura enum (può chiamare il metodo mutatore e cambiare un valore di qualsiasi chiave) ... quindi usa const x = Object.freeze({key: 'value'})invece per ottenere qualcosa che sembra e si comporta come enum in ES6
Yurii Rabeshko,

È necessario passare una stringa per ottenere il valore, come hai fatto con colors.get ('RED'). Qual è soggetto a errori.
adrian oviedo,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.