Javascript 中的枚举与 ES6

Enums in Javascript with ES6

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

我能想到的最好的是:

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

const 使 Colors 不被重新分配,冻结它可以防止键和值发生变化。我正在使用 Symbols,因此 Colors.RED 不等于 0 或除自身之外的任何其他内容。

这个公式有问题吗?有没有更好的方法?


(我知道这个问题有点重复,但是所有的previous Q/As都已经很老了,ES6给了我们一些新的能力。)


编辑:

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

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

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

通过使用对象引用作为值,您可以获得与符号相同的碰撞避免。

Is there a problem with this formulation?

我没看到。

Is there a better way?

我会将这两个语句合二为一:

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

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

您可以查看 Enumify,一个非常好的 ES6 枚举库。

勾选how TypeScript does it。 基本上他们会做以下事情:

const MAP = {};

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

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

使用符号,冻结对象,随心所欲。

你也可以使用 es6-enum 包 (https://www.npmjs.com/package/es6-enum)。它非常易于使用。请参阅以下示例:

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

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

如果你不需要 ES6并且可以使用Typescript,它有一个很好的enum:

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

你可以使用 ES6 Map

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

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

这是我个人的做法。

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

也许这个解决方案? :)

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

虽然使用 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 enum annotation.

记录的枚举属性和类型

平等按预期工作,Colors.RED === Colors.REDtrueColors.RED === Colors.BLUEfalse

我更喜欢@tonethar 的方法,通过一些增强和挖掘来更好地理解 ES6/Node.js 生态系统的底层。在服务器端的背景下,我更喜欢围绕平台原语的功能风格的方法,这最大限度地减少了代码膨胀,由于引入了新类型和增加而导致的死亡阴影状态管理谷的滑坡可读性——使解决方案和算法的意图更加清晰。

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

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

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

这是我在 Java 脚本中实现的 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>


更新

这是满足 MDN 要求的最新版本。

根据 MDN 的推荐 Object.prototype.__defineGetter__ has been replaced by Object.defineProperty

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.

编辑: 为枚举值添加了一个原型 (Enum.__prototype) 来处理道具的 JSON 序列化。

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')
      JSON.stringify(red).should.equal('{"hex":"#F00"}')
    })

    it('Test: Color.BLUE', () => {
      let blue = Color.BLUE
      chai.assert.isNotNull(blue)
      blue.getHex().should.equal('#0F0')
      JSON.stringify(blue).should.equal('{"hex":"#0F0"}')
    })

    it('Test: Color.GREEN', () => {
      let green = Color.GREEN
      chai.assert.isNotNull(green)
      green.getHex().should.equal('#00F')
      JSON.stringify(green).should.equal('{"hex":"#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.create(Enum.__prototype(props))

    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])
  }
}

Enum.__prototype = (props) => ({
  toJSON() {
    return props;
  },
  toString() {
    return JSON.stringify(props);
  }
});

/* @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>

2020 年 5 月 11 日更新:
修改为包括静态字段和方法以更接近地复制“真实”枚举行为。

如果您打算更新,我建议您尝试使用我称之为“枚举 Class”的东西(除非您无法接受任何浏览器或运行时环境限制)。它基本上是一个 非常简单 和干净的 class,它使用私有字段和有限的访问器来模拟枚举的行为。当我想在枚举中构建更多功能时,我有时会在 C# 中这样做。

我意识到私有 class 字段目前仍处于实验阶段,但它似乎可以用于创建具有不可变 fields/properties 的 class。浏览器支持也不错。唯一不支持它的“主要”浏览器是 Firefox(我相信他们很快就会支持)和 IE(谁在乎)。

免责声明:
我不是开发人员。我只是把它放在一起解决我在做个人项目时JS中不存在的枚举的限制。

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

您现在应该可以直接调用您的枚举了。

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

结合使用私有字段和有限访问器意味着现有的枚举值得到了很好的保护(它们现在基本上是常量)。

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

这是一个 Enum 工厂,它通过使用命名空间和 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"));

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

我使用与 VSCode / VSCodium 兼容的 JSDoc 增强的字符串。它高效、简单且安全例如:

/** @typedef { 'red' | 'green' | 'blue' } color */

/** @type {color} */
let color = 'red'

/**
 * @param {color} c
 */
function f(c) {}

另一种使用 ES2022 的列表方法

class Enum {
  static toEnum() {
    const enumMap = new Map();
    for (const [key, value] of Object.entries(this)) {
      enumMap.set(key, value);
    }
    this.enumMap = enumMap;
  }

  static [Symbol.iterator]() {
    return this.enumMap[Symbol.iterator]();
  }

  static getValueOf(str) {
    return this.enumMap.get(str);
  }
}


class ActionTypes extends Enum {
  static REBALANCE = Symbol("REBALANCE");
  static MESSAGE = Symbol("MESSAGE");
  static FETCH = Symbol("FETCH");
  static { this.toEnum() }
}