如何在JavaScript中写枚举(翻译)

这周用RN写业务的时候 遇到了写枚举的需求, 翻了一下JS高级程序设计还有犀牛书发现没有这方面的内容, 于是去Google了一下, 翻到了在SOF上面的一个高票问题 Enums in JavaScript?
在下面的一个回答的里面又有一个高票的评论, 就是我下面翻译的这篇文章, 讲到了为什么JS没有枚举以及对于模拟枚举不同方案的考量, 并且给出了最优的模式.

原文地址:Enums in Javascript


译文:

像这样定义你的枚举:

var SizeEnum = {
  SMALL: 1,
  MEDIUM: 2,
  LARGE: 3,
};

然后他们用起来是这样的:

var mySize = SizeEnum.SMALL;

如果你想让枚举值持有属性,你可以把他们加到一个额外的属性上面:

var SizeEnum = {
  SMALL: 1,
  MEDIUM: 2,
  LARGE: 3,
  properties: {
    1: {name: "small", value: 1, code: "S"},
    2: {name: "medium", value: 2, code: "M"},
    3: {name: "large", value: 3, code: "L"}
  }
};

他们用起来像这样:

var mySize = SizeEnum.MEDIUM;
var myCode = SizeEnum.properties[mySize].code; // myCode == "M"

背景信息

上面的JS枚举写法是我深思熟虑之后得出的. 这个写法尝试在 1. 使用基本类型作为枚举值(序列与反序列化的安全)以及 2. 使用对象作为枚举值(可以在这个对象里面存储属性)方面合并各个语言最好的设计.
继续往下读看我是如何得出这个写法的.

重新认识枚举

我最近偶然看到了一个我几年前回到回答过的SOF问题, 在看了下面的评论解答之后我做了更多的思考, 认为这个问题值得去写一篇文章.
所以是什么问题呢?

在JavaScript中枚举最好的写法是什么

首先, 在回答这个问题之前, 我们需要知道枚举是什么以及写一个枚举在JavaScript代表什么.所以来让我们看一下枚举的定义:

什么是枚举

在计算机编程中,枚举类型是一个由一组叫做元素, 成员, 枚举成员的值组成的数据类型.枚举成员的名字在语言中通常充当常量的标识符.枚举类型的变量可以被任意的枚举成员所赋值.

-- Wikipedia: Enumerated type

并且好的例子总是符合这个定义的:

enum WeekDay = {MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY};

所以, 总结下来就是: 一个枚举是一种从事先定义好的一组常量转换而来的遵从制约的变量.在上述例子中, WeekDay是枚举, 而MONDAY, TUESDAY等是组内的常量, 也被叫做枚举成员. 如果我们声明一个变量如下:

WeekDay payDay;

那么我们就可以给他赋值像MONDAY, TUESDAY一直到SUNDAY, 但是不是像其他如12或者"labour day"这样.
那么这样就带来了一个问题.

在JavaScript中他是无法实现的

JavaScript是一种弱类型语言, 这代表着你不能在声明一个变量的时候事先规定他的类型.在Java中你可能这么写:

int i; // 声明一个变量,变量名称是i, 持有一个整型的值

如果此时你尝试将他赋值为一个字符串:

i = "Hello World";

编译器会抛出一个错误并且停止运行工程.
但是在JavaScript中, 情况却是不一样的:

var i;
i = 10;
i = "Hello World";
i = 3.1415;
i = true;
i = ['my', 'array'];
i = {look: 'at', my: 'object'};

就像我们看到的我们声明了一个变量i(使用关键字var), 但是他是运行时的类型并不是确定的. 我们可以给这个变量赋任意值. 在维基上面的措辞有一点粗劣, 但是如果你看了这一句:

枚举类型的变量可以被任意的枚举成员所赋值

那么就知道, 我们任意的枚举成员都可以成为变量的值, 但是前提是变量需要是枚举类型所事先声明的. 所以, 他规定了变量需要被类型所事先声明. 那么在JavaScript中, 这个是做不到的. 所以就是说我们其实是无法在JavaScript中写出真正的符合定义的枚举的. 但是我们可以模拟枚举以此来获得他提供给其他语言, 比如说C的一些便利, 但是, 记住, 他仅仅是模拟并且是语法糖.

在JavaScript下'枚举'的写法

我是一个Java程序员, Java是强类型语言, 但是碰巧Java在很长时间也并没有枚举, 于是Java程序员们提出了很多方案去模拟枚举.

提供名称便利的常量.

类似我们写在类最上面的常量.

public static final int DAYS_MONDAY = 0;
public static final int DAYS_TUESDAY = 1;
// ..
public static final int DAYS_SUNDAY = 6;

常量组成的类

像上面的例子一样, 但是将常量封装成一个专用的类来促进常量的重用.

public class DaysEnum {
  public static final int MONDAY = 0;
  public static final int TUESDAY = 1;
  // ..
  public static final int SUNDAY = 6;
}

实例作为常量的类

这个是最先进的模拟思路, 并且在一定程度上启发了Java枚举类型的最终解决方案. 他用一个拥有私有构造器的类(私有构造器意味着外部不会生成这样的实例), 并且用这些实例作为枚举成员:

public class DaysEnum {
  private DaysEnum() {}
  public static final DaysEnum MONDAY = new DaysEnum();
  public static final DaysEnum TUESDAY = new DaysEnum();
  // ..
  public static final DaysEnum SUNDAY = new DaysEnum();
}

这种用实例作为枚举成员的解决方案极其优雅. 它使得对于枚举的模拟实现了真正的类型安全.

对比上面的两种方案, 如变量可以持有一个整型的枚举值的话:

int payDay = DAYS_FRIDAY; // variation 1
int payDay = DaysEnum.FRIDAY; // variation 2

他始终有被错误赋值的情况, 比如赋值为128. 但是相反, 第三种解决方案限制了赋值的范围, 只可以用写在类中的枚举成员来进行赋值:

DaysEnum payDay = DaysEnum.FRIDAY; // ok
DaysEnum payDay = 128; // compiler error

并且, 第三种解决方案给我们提供了额外的便利, 我们可以添加额外的字段, 比如, 去创建一天名字的字段, 或者甚至是一个方法(比如isWeekendDay()).
所以, 在了解了Java的这些模式之后, 我在StackOverflow上面建议使用第三种解决方案来写JavaScript的枚举. 这个答案始终是投票最高的, 但是我现在不再认同自己的这个解答了. 那么, 让我解释为什么, 并且, 告诉你我个人认为在JavaScript中怎样写枚举会有最大的好处以及最少的问题.

所以在JavaScript中第三种模式怎么写并且他有什么问题呢

下面是第三种模式在JavaScript中的写法:

var DaysEnum = {
  MONDAY: {}, // 可以添加属性以及方法
  TUESDAY: {},
  // ..
  SUNDAY: {}
};

但是像我在上面说的, 我不再推荐这种写法.

为什么呢?

因为, 正如jcollum在那个问题其他答案的评论下所提到的,这种方法在数据被序列化的时候会产生问题. 为了理解为什么, 让我们看一下当我们给对象用枚举中的一个枚举成员赋值时会发生什么:

var myObject = {
  payDay: DaysEnum.FRIDAY
};

var yesterday = DaysEnum.THURSDAY, today = DaysEnum.FRIDAY;
if (yesterday == myObject.payDay)
  alert("Yesterday was pay day... but not today...");
else if (today == myObject.payDay)
  alert("Today is pay day! Yippie!!!");
else
  alert("Neither yesterday nor today are pay days... I'm broke!");

那么, 到目前为止, 他可能是输出"Today is pay day! Yippie!!!".
但是让我们看一下当我们序列化myObject到JSON会发生什么并且将它反序列化:

var serialized = JSON.stringify(myObject);
alert("serialized myObject: " + serialized);
var deserializedObject = JSON.parse(serialized);
if (yesterday == deserializedObject.payDay)
  alert("Yesterday was pay day... but not today...");
else if (today == deserializedObject.payDay)
  alert("Today is pay day! Yippie!!!");
else
  alert("Neither yesterday nor today are pay days... I'm broke!");

最终的结果是输出了"Neither yesterday nor today are pay days... I'm broke!". 输出这个的原因是在反序列化的时候创建出了新的对象作为payDay的值.
这个新的对象和DaysEnum.FRIDAY不相等, 所以所有前面的匹配都失败了, 最终程序跑进最后一个else分支里面.

或许有方法可以解决这个问题, 但是值得么?
我会说不值得. 而去序列以及反序列化枚举值是一个不能忽视的问题. 这就是我说不推荐使用这个模式的一个原因.而, 正相反的是, 在上面提到的模式2不存在刚才序列化及反序列化的问题:

var DaysEnum = {
  MONDAY: "monday",
  TUESDAY: "tuesday",
  // ..
  SUNDAY: "sunday"
};

(或者你可以用数字代替字符串, 他们的最终效果是一样的)
让我们检验一下他的序列化是否是安全的:

var myObject = {
  payDay: DaysEnum.FRIDAY
};

var serialized = JSON.stringify(myObject);
alert("serialized myObject: " + serialized);
var deserializedObject = JSON.parse(serialized);
if (yesterday == deserializedObject.payDay)
  alert("Yesterday was pay day... but not today...");
else if (today == deserializedObject.payDay)
  alert("Today is pay day! Yippie!!!");
else
  alert("Neither yesterday not today are pay days... I'm broke!");

他的运行结果正式我们预期得到的"Today is pay day! Yippie!!!".

但是我想要自定义字段和方法

是的, 如果做不到这个的话很尴尬...去给枚举成员字段以及方法的特性十分诱人.
我们可以这么做:

var SizeEnum = {
  SMALL: {name: "small", value: 1, code: "S"},
  MEDIUM: {name: "medium", value: 2, code: "M"},
  LARGE: {name: "large", value: 3, code: "L"},
};

但是, 像刚才说的, 这样做的话, 在序列化和反序列化方面会有问题, 所以, 现在怎么办?
其实, 我们可以将属性加到另外一个对象上面, 像这样:

var SizeEnum = {
  SMALL: 1,
  MEDIUM: 2,
  LARGE: 3,
  properties: {
    1: {name: "small", value: 1, code: "S"},
    2: {name: "medium", value: 2, code: "M"},
    3: {name: "large", value: 3, code: "L"}
  }
};

这样, 我们就可以接受到枚举的属性:

var mySize = SizeEnum.MEDIUM;
var myCode = SizeEnum.properties[mySize].code; // myCode == "M"

确实, 这样的模式不够优雅但是这就是现状.我们必须去做一个艰难的选择.
我认为, 相比较于写法优雅的实例属性, 序列化与反序列化的安全性是更加重要的.

Object.freeze

在拥有枚举的类型安全的语言中, 枚举被理解为常量. 枚举成员以及他们所被赋值的常量都不会改变.但是在JavaScript中, 我们可能会在任意的时间重写任意的常量, 或者在枚举成员中新添加一个. 如果你想避免这些情况, 你可能需要看一下Object.freeze:

Object.freeze()将一个对象进行冰冻: 他的意思是, 阻止添加新的属性, 阻止移除已存在的属性; 防止存在的属性,或其可数性、可配置性,或可性,被改变.
实际上, 这个对象将有效的不可变了. 这个方法返回的是被冰冻的对象.

这不就是我们想要的么.
因为并不是所有的浏览器都支持这个特性, 所以你在使用它的时候应该先检查一下他是否存在.

if (Object.freeze)
  Object.freeze(DaysEnum);

你可能感兴趣的:(如何在JavaScript中写枚举(翻译))