以下内容是否会使对象满足枚举在JavaScript中的所有特性?类似于:

my.namespace.ColorEnum = {
  RED : 0,
  GREEN : 1,
  BLUE : 2
}

// later on

if(currentColor == my.namespace.ColorEnum.RED) {
  // whatever
}

还是有其他方法可以做到这一点?


当前回答

我提出了一种以Java中的枚举为模型的方法。这些是类型安全的,因此您也可以执行instanceof检查。

可以这样定义枚举:

var Days = Enum.define("Days", ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]);

Days现在引用Days枚举:

Days.Monday instanceof Days; // true

Days.Friday.name(); // "Friday"
Days.Friday.ordinal(); // 4

Days.Sunday === Days.Sunday; // true
Days.Sunday === Days.Friday; // false

Days.Sunday.toString(); // "Sunday"

Days.toString() // "Days { Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday } "

Days.values().map(function(e) { return e.name(); }); //["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
Days.values()[4].name(); //"Friday"

Days.fromName("Thursday") === Days.Thursday // true
Days.fromName("Wednesday").name() // "Wednesday"
Days.Friday.fromName("Saturday").name() // "Saturday"

实施:

var Enum = (function () {
    /**
     * Function to define an enum
     * @param typeName - The name of the enum.
     * @param constants - The constants on the enum. Can be an array of strings, or an object where each key is an enum
     * constant, and the values are objects that describe attributes that can be attached to the associated constant.
     */
    function define(typeName, constants) {

        /** Check Arguments **/
        if (typeof typeName === "undefined") {
            throw new TypeError("A name is required.");
        }

        if (!(constants instanceof Array) && (Object.getPrototypeOf(constants) !== Object.prototype)) {

            throw new TypeError("The constants parameter must either be an array or an object.");

        } else if ((constants instanceof Array) && constants.length === 0) {

            throw new TypeError("Need to provide at least one constant.");

        } else if ((constants instanceof Array) && !constants.reduce(function (isString, element) {
                return isString && (typeof element === "string");
            }, true)) {

            throw new TypeError("One or more elements in the constant array is not a string.");

        } else if (Object.getPrototypeOf(constants) === Object.prototype && !Object.keys(constants).reduce(function (isObject, constant) {
                return Object.getPrototypeOf(constants[constant]) === Object.prototype;
            }, true)) {

            throw new TypeError("One or more constants do not have an associated object-value.");

        }

        var isArray = (constants instanceof Array);
        var isObject = !isArray;

        /** Private sentinel-object used to guard enum constructor so that no one else can create enum instances **/
        function __() { };

        /** Dynamically define a function with the same name as the enum we want to define. **/
        var __enum = new Function(["__"],
            "return function " + typeName + "(sentinel, name, ordinal) {" +
                "if(!(sentinel instanceof __)) {" +
                    "throw new TypeError(\"Cannot instantiate an instance of " + typeName + ".\");" +
                "}" +

                "this.__name = name;" +
                "this.__ordinal = ordinal;" +
            "}"
        )(__);

        /** Private objects used to maintain enum instances for values(), and to look up enum instances for fromName() **/
        var __values = [];
        var __dict = {};

        /** Attach values() and fromName() methods to the class itself (kind of like static methods). **/
        Object.defineProperty(__enum, "values", {
            value: function () {
                return __values;
            }
        });

        Object.defineProperty(__enum, "fromName", {
            value: function (name) {
                var __constant = __dict[name]
                if (__constant) {
                    return __constant;
                } else {
                    throw new TypeError(typeName + " does not have a constant with name " + name + ".");
                }
            }
        });

        /**
         * The following methods are available to all instances of the enum. values() and fromName() need to be
         * available to each constant, and so we will attach them on the prototype. But really, they're just
         * aliases to their counterparts on the prototype.
         */
        Object.defineProperty(__enum.prototype, "values", {
            value: __enum.values
        });

        Object.defineProperty(__enum.prototype, "fromName", {
            value: __enum.fromName
        });

        Object.defineProperty(__enum.prototype, "name", {
            value: function () {
                return this.__name;
            }
        });

        Object.defineProperty(__enum.prototype, "ordinal", {
            value: function () {
                return this.__ordinal;
            }
        });

        Object.defineProperty(__enum.prototype, "valueOf", {
            value: function () {
                return this.__name;
            }
        });

        Object.defineProperty(__enum.prototype, "toString", {
            value: function () {
                return this.__name;
            }
        });

        /**
         * If constants was an array, we can the element values directly. Otherwise, we will have to use the keys
         * from the constants object.
         */
        var _constants = constants;
        if (isObject) {
            _constants = Object.keys(constants);
        }

        /** Iterate over all constants, create an instance of our enum for each one, and attach it to the enum type **/
        _constants.forEach(function (name, ordinal) {
            // Create an instance of the enum
            var __constant = new __enum(new __(), name, ordinal);

            // If constants was an object, we want to attach the provided attributes to the instance.
            if (isObject) {
                Object.keys(constants[name]).forEach(function (attr) {
                    Object.defineProperty(__constant, attr, {
                        value: constants[name][attr]
                    });
                });
            }

            // Freeze the instance so that it cannot be modified.
            Object.freeze(__constant);

            // Attach the instance using the provided name to the enum type itself.
            Object.defineProperty(__enum, name, {
                value: __constant
            });

            // Update our private objects
            __values.push(__constant);
            __dict[name] = __constant;
        });

        /** Define a friendly toString method for the enum **/
        var string = typeName + " { " + __enum.values().map(function (c) {
                return c.name();
            }).join(", ") + " } ";

        Object.defineProperty(__enum, "toString", {
            value: function () {
                return string;
            }
        });

        /** Freeze our private objects **/
        Object.freeze(__values);
        Object.freeze(__dict);

        /** Freeze the prototype on the enum and the enum itself **/
        Object.freeze(__enum.prototype);
        Object.freeze(__enum);

        /** Return the enum **/
        return __enum;
    }

    return {
        define: define
    }

})();

其他回答

这是我对一个(标记的)Enum工厂的看法。这是一个工作演示。

/*
 * Notes: 
 * The proxy handler enables case insensitive property queries
 * BigInt is used to enable bitflag strings /w length > 52
*/
function EnumFactory() {
  const proxyfy = {
    construct(target, args) { 
      const caseInsensitiveHandler = { 
          get(target, key) {
          return target[key.toUpperCase()] || target[key];  
        } 
      };
      const proxified = new Proxy(new target(...args), caseInsensitiveHandler ); 
      return Object.freeze(proxified);
    },
  }
  const ProxiedEnumCtor = new Proxy(EnumCtor, proxyfy);
  const throwIf = (
      assertion = false, 
      message = `Unspecified error`, 
      ErrorType = Error ) => 
      assertion && (() => { throw new ErrorType(message); })();
  const hasFlag = (val, sub) => {
    throwIf(!val || !sub, "valueIn: missing parameters", RangeError);
    const andVal = (sub & val);
    return andVal !== BigInt(0) && andVal === val;
  };

  function EnumCtor(values) {
    throwIf(values.constructor !== Array || 
            values.length < 2 || 
        values.filter( v => v.constructor !== String ).length > 0,
      `EnumFactory: expected Array of at least 2 strings`, TypeError);
    const base = BigInt(1);
    this.NONE = BigInt(0);
    values.forEach( (v, i) => this[v.toUpperCase()] = base<<BigInt(i) );
  }

  EnumCtor.prototype = {
    get keys() { return Object.keys(this).slice(1); },
    subset(sub) {
      const arrayValues = this.keys;
      return new ProxiedEnumCtor(
        [...sub.toString(2)].reverse()
          .reduce( (acc, v, i) => ( +v < 1 ? acc : [...acc, arrayValues[i]] ), [] )
      );
    },
    getLabel(enumValue) {
      const tryLabel = Object.entries(this).find( value => value[1] === enumValue );
      return !enumValue || !tryLabel.length ? 
        "getLabel: no value parameter or value not in enum" :
        tryLabel.shift();
    },
    hasFlag(val, sub = this) { return hasFlag(val, sub); },
  };
  
  return arr => new ProxiedEnumCtor(arr);
}

从1.8.5开始,可以密封和冻结物体,因此定义如下:

const DaysEnum = Object.freeze({"monday":1, "tuesday":2, "wednesday":3, ...})

or

const DaysEnum = {"monday":1, "tuesday":2, "wednesday":3, ...}
Object.freeze(DaysEnum)

瞧!JS枚举。

然而,这并不能阻止您为变量赋值,这通常是enums的主要目标:

let day = DaysEnum.tuesday
day = 298832342 // goes through without any errors

确保更强的类型安全性(使用enums或其他)的一种方法是使用TypeScript或Flow等工具。

不需要引用,但我保留了它们以保持一致性。

您只需要使用object.freeze(<your_object>)创建一个不可变的对象:

export const ColorEnum = Object.freeze({
    // you can only change the property values here
    // in the object declaration like in the Java enumaration
    RED: 0,
    GREEN: 1,
    BLUE: 2,
});

ColorEnum.RED = 22    // assigning here will throw an error
ColorEnum.VIOLET = 45 // even adding a new property will throw an error

这是我知道的一个旧版本,但它通过TypeScript接口实现的方式是:

var MyEnum;
(function (MyEnum) {
    MyEnum[MyEnum["Foo"] = 0] = "Foo";
    MyEnum[MyEnum["FooBar"] = 2] = "FooBar";
    MyEnum[MyEnum["Bar"] = 1] = "Bar";
})(MyEnum|| (MyEnum= {}));

这使您能够查找返回1的MyEnum.Bar和返回“Bar”的MyEnum[1],无论声明的顺序如何。

在ES7中,您可以依靠静态属性执行优雅的ENUM:

class ColorEnum  {
    static RED = 0 ;
    static GREEN = 1;
    static BLUE = 2;
}

然后

if (currentColor === ColorEnum.GREEN ) {/*-- coding --*/}

(使用类而不是文本对象)的优点是有一个父类Enum,然后所有Enum都将扩展该类。

 class ColorEnum  extends Enum {/*....*/}