简体   繁体   中英

Enums in Javascript with ES6

I'm rebuilding an old Java project in Javascript, and realized that there's no good way to do enums in JS.

The best I can come up with is:

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

The const keeps Colors from being reassigned, and freezing it prevents mutating the keys and values. I'm using Symbols so that Colors.RED is not equal to 0 , or anything else besides itself.

Is there a problem with this formulation? Is there a better way?


(I know this question is a bit of a repeat, but all the previous Q/As are quite old, and ES6 gives us some new capabilities.)


EDIT:

Another solution, which deals with the serialization problem, but I believe still has realm issues:

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

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

By using object references as the values, you get the same collision-avoidance as Symbols.

Is there a problem with this formulation?

I don't see any.

Is there a better way?

I'd collapse the two statements into one:

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

If you don't like the boilerplate, like the repeated Symbol calls, you can of course also write a helper function makeEnum that creates the same thing from a list of names.

Whilst using Symbol as the enum value works fine for simple use cases, it can be handy to give properties to enums. This can be done by using an Object as the enum value containing the properties.

For example we can give each of the Colors a name and hex value:

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

Including properties in the enum avoids having to write switch statements (and possibly forgetting new cases to the switch statements when an enum is extended). The example also shows the enum properties and types documented with the JSDoc enum annotation .

Equality works as expected with Colors.RED === Colors.RED being true , and Colors.RED === Colors.BLUE being false .

As mentioned above, you could also write a makeEnum() helper function:

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

Use it like this:

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

Check how TypeScript does it . Basically they do the following:

const MAP = {};

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

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

Use symbols, freeze object, whatever you want.

您可以检查Enumify ,它是ES6枚举的一个非常好的功能齐全的库。

This is my personal approach.

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

Update 11.05.2020:
Modified to include static fields and methods to closer replicate "true" enum behavior.

If you're planning on updating I would recommend trying to use what I call an "Enum Class" (barring any browser or runtime env limitations you can't accept). It's basically a very simple and clean class that uses private fields and limited accessors to simulate the behavior of an enum. This is something I sometimes do in C# when I want to build more functionality into an enum.

I realize private class fields are still experimental at this point but it seems to work for the purposes of creating a class with immutable fields/properties. Browser support is decent as well. The only "major" browsers that don't support it are Firefox (which I'm sure they will soon) and IE (who cares).

DISCLAIMER :
I am not a developer. I just put this together to solve the limitations of nonexistent enums in JS when I was working on a personal project.

Sample Class

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

You should now be able to call your enums directly.

Colors.RED; // 0
Colors.GREEN; // 1
Colors.BLUE; // 2

The combination of using private fields and limited accessors means that the existing enum values are well protected (they're essentially constants now).

Colors.RED = 10 // Colors.RED is still 0
Colors._RED = 10 // Colors.RED is still 0
Colors.#_RED = 10 // Colors.RED is still 0

If you don't need pure ES6 and can use Typescript, it has a nice enum :

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

Here is my implementation of a Java enumeration in JavaScript.

I also included unit tests.

 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>


Update

Here is a more up-to-date version that satisfies MDN.

The Object.prototype.__defineGetter__ has been replaced by Object.defineProperty per MDN's recomendation:

This feature is deprecated in favor of defining getters using the object initializer syntax or the Object.defineProperty() API. While this feature is widely implemented, it is only described in the ECMAScript specification because of legacy usage. This method should not be used since better alternatives exist.

 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 = [] 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.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]) } } /* @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>

Maybe this solution ? :)

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

I prefer @tonethar's approach, with a little bit of enhancements and digging for the benefit of understanding better the underlyings of the ES6/Node.js ecosystem. With a background in the server side of the fence, I prefer the approach of functional style around platform's primitives, this minimizes the code bloat, the slippery slope into the state's management valley of the shadow of death due to the introduction of new types and increases the readability - makes more clear the intent of the solution and the algorithm.

Solution with 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

Here is my approach, including some helper methods

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

Here is an Enum factory that avoids realm issues by using a namespace and 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"));

I used this approach which is modeled after enums in C# and Java. Note that Enum is a subclass of Function , so the enum values are objects, and you can add instance methods to an enum.

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

By using class instances as the values, you get the same collision-avoidance as Symbols.

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

you can also use es6-enum package ( https://www.npmjs.com/package/es6-enum ). It's very easy to use. See the example below:

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

Colors.red returns Symbol(red)

You could use ES6 Map

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

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

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM