ES6的JavaScript中的枚举

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

我能想到的最好的办法是:

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

const保持Colors不被重新分配,并且冻结它防止变异键和值。我使用符号是为了让Colors.RED不等于0,或者除了它本身之外的任何东西。

这个提法有问题吗?有没有更好的办法?


(我知道这个问题有点重复,但之前所有的问答都很老了,ES6给了我们一些新的能力。


编辑:

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

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


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

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

166625 次浏览

这个提法有问题吗?

我什么都没看到。

有没有更好的办法?

我会把这两句话合并成一句:

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

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

您可以查看枚举,这是一个非常好的、功能强大的ES6枚举库。

检查TypeScript是如何做到的?。 基本上他们会做以下事情:

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 = Object.create(null);
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地图

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作为包含属性的枚举值来完成。

例如,我们可以为ABC_0的每个_指定一个名称和十六进制值:

/**
* 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枚举注释记录的枚举属性和类型。

Colors.RED === Colors.REDtrueColors.RED === Colors.BLUEfalse时,等式按预期工作。

我更喜欢@ToneTar的方法,通过一些增强和挖掘,可以更好地理解ES6/Node.JS生态系统的基础。在服务器端的背景下,我更喜欢围绕平台原语的函数风格的方法,这最大限度地减少了代码膨胀,由于新类型的引入而滑入国家管理的死亡阴影之谷,并增加了可读性-使解决方案和算法的意图更加清晰。

TDDES6node.JS洛达什开玩笑巴贝尔埃斯林特的解决方案

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

下面是我在JavaScript中实现的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__已替换为Object.defineProperty

不推荐使用此功能,而推荐使用对象初始化器语法或Object.defineProperty() API来定义getter.虽然此功能被广泛实现,但由于传统用法,它只在ECMAScript规范中进行了描述。由于存在更好的替代方法,因此不应使用此方法。

编辑:为枚举值添加了一个原型(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>

更新11.05.2020:
修改为包括静态字段和方法,以更接近复制“ true ”枚举行为.

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

我意识到私有类字段在这一点上仍然是实验性的,但它似乎可以用于创建具有不可变字段/属性的类。浏览器支持也很不错。唯一的";主要";不支持它的浏览器是Firefox(我相信他们很快就会支持)和IE(谁在乎呢)。

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

示例类

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