如何在 JavaScript 中实现具有关联值的类似 Swift 的枚举?
How to implement Swift-like enums with associated values in JavaScript?
Swift 语言具有出色的枚举支持。不仅可以用 case 定义标准枚举,而且 case 可以有可选值 "associated to them."
例如,取自 Swift 文档:
enum Barcode {
case UPCA(Int, Int, Int, Int)
case QRCode(String)
case Other
}
这样就可以通过传入一个值来创建条形码枚举,如下所示:
var productBarcode = Barcode.UPCA(8, 85909, 51226, 3)
以及稍后 productBarcode
上的 switch
以检索关联值(int
的元组)。
我一直在尝试在 JavaScript(尤其是 ES5)中实现这种 enum
系统,但我碰壁了。构建枚举系统的最佳方式是什么,尤其是具有关联值的系统?
枚举在我所知道的大多数语言中的工作方式并不完全相同。通常它们更像是一种将值键入为这些状态之一的方法。就像从一组可能的值中选择一个值一样。并确保这样做的类型安全,这与普通整数不同。
你在代码中发布的内容,我会调用带有工厂方法的普通对象。
由于语言不支持它们,因此您必须以尽可能满足您需求的方式实现它们。所以总结一下你期望的行为。
同时,基于我在 swift 枚举中找到的描述的实现。希望它接近您的期望:
var odp = {
ENUMERABLE: 4,
//two helper with Object.defineProperty.
value: function(obj, prop, v, flags){
this.configurable = Boolean(flags & odp.CONFIGURABLE);
this.writable = Boolean(flags & odp.WRITABLE);
this.enumerable = Boolean(flags & odp.ENUMERABLE);
this.value = v;
Object.defineProperty(obj, prop, this);
this.value = null; //v may be a function or an object: remove the reference
return obj;
}.bind({ //caching the basic definition
value: null,
configurable: false,
writable: false,
enumerable: false
}),
accessor: function(obj, prop, getter, setter){
this.get = getter || undefined;
this.set = setter || undefined;
Object.defineProperty(obj, prop, this);
this.get = null;
this.set = null;
return obj;
}.bind({ get: null, set: null })
}
//make these values immutable
odp.value(odp, "CONFIGURABLE", 1, odp.ENUMERABLE);
odp.value(odp, "WRITABLE", 2, odp.ENUMERABLE);
odp.value(odp, "ENUMERABLE", 4, odp.ENUMERABLE);
//Policy:
//1. I don't f*** care wether the keys on the definition are own or inherited keys.
//since you pass them to me, I suppose you want me to process them.
//2. If i find some undefined-value i ignore it, as if it wasn't there.
//use null to represent some "empty" value
//name and extendProto are optional
function Enum(name, description, extendProto){
var n = name, d = description, xp=extendProto;
if(n && typeof n === "object") xp=d, d = n, n = null;
var xpf = typeof xp === "function" && xp;
var xpo = typeof xp === "object" && xp;
function type(){
throw new Error("enums are not supposed to be created manually");
}
//abusing filter() as forEach()
//removing the keys that are undefined in the same step.
var keys = Object.keys(d).filter(function(key){
var val = d[key];
if(val === undefined) return false;
var proto = Object.create(type.prototype);
//your chance to extend the particular prototype with further properties
//like adding the prototype-methods of some other type
var props = xpf || xpo && xpo[key];
if(typeof props === "function")
props = props.call(type, proto, key, val);
if(props && typeof props === "object" && props !== proto && props !== val){
var flags = odp.CONFIGURABLE+odp.WRITABLE;
for(var k in props) props[k]===undefined || odp.value(proto, k, props[k], flags);
if("length" in props) odp.value(props, "length", props.length, flags);
}
if(typeof val === "function"){
//a factory and typedefinition at the same type
//call this function to create a new object of the type of this enum
//and of the type of this function at the same time
type[key] = function(){
var me = Object.create(proto);
var props = val.apply(me, arguments);
if(props && typeof props === "object" && props !== me){
for(var k in props) props[k]===undefined || odp.value(me, k, props[k], odp.ENUMERABLE);
if("length" in props) odp.value(me, "length", props.length);
}
return me;
}
//fix the fn.length-property for this factory
odp.value(type[key], "length", val.length, odp.CONFIGURABLE);
//change the name of this factory
odp.value(type[key], "name", (n||"enum")+"{ "+key+" }" || key, odp.CONFIGURABLE);
type[key].prototype = proto;
odp.value(proto, "constructor", type[key], odp.CONFIGURABLE);
}else if(val && typeof val === "object"){
for(var k in val) val[k] === undefined || odp.value(proto, k, val[k]);
if("length" in val) odp.value(proto, "length", val.length);
type[key] = proto;
}else{
//an object of the type of this enum that wraps the primitive
//a bit like the String or Number or Boolean Classes
//so remember, when dealing with this kind of values,
//you don't deal with actual primitives
odp.value(proto, "valueOf", function(){ return val; });
type[key] = proto;
}
return true;
});
odp.value(type, "name", n || "enum[ " + keys.join(", ") + " ]", odp.CONFIGURABLE);
Object.freeze(type);
return type;
}
注意,此代码可能需要进一步修改。示例:
工厂
function uint(v){ return v>>>0 }
var Barcode = Enum("Barcode", {
QRCode: function(string){
//this refers to an object of both types, Barcode and Barcode.QRCode
//aou can modify it as you wish
odp.value(this, "valueOf", function(){ return string }, true);
},
UPCA: function(a,b,c,d){
//you can also return an object with the properties you want to add
//and Arrays, ...
return [
uint(a),
uint(b),
uint(c),
uint(d)
];
//but beware, this doesn't add the Array.prototype-methods!!!
//event this would work, and be processed like an Array
return arguments;
},
Other: function(properties){
return properties; //some sugar
}
});
var productBarcode = Barcode.UPCA(8, 85909, 51226, 3);
console.log("productBarcode is Barcode:", productBarcode instanceof Barcode); //true
console.log("productBarcode is Barcode.UPCA:", productBarcode instanceof Barcode.UPCA); //true
console.log("productBarcode is Barcode.Other:", productBarcode instanceof Barcode.Other); //false
console.log("accessing values: ", productBarcode[0], productBarcode[1], productBarcode[2], productBarcode[3], productBarcode.length);
Array.prototype.forEach.call(productBarcode, function(value, index){
console.log("index:", index, " value:", value);
});
对象和基元
var indices = Enum({
lo: { from: 0, to: 13 },
hi: { from: 14, to: 42 },
avg: 7
});
var lo = indices.lo;
console.log("lo is a valid index", lo instanceof indices);
console.log("lo is indices.lo", lo === indices.lo);
//indices.lo always references the same Object
//no function-call, no getter!
var avg = indices.avg; //beware, this is no primitive, it is wrapped
console.log("avg is a valid index", avg instanceof indices);
console.log("comparison against primitives:");
console.log(" - typesafe", avg === 7); //false, since avg is wrapped!!!
console.log(" - loose", avg == 7); //true
console.log(" - typecast+typesafe", Number(avg) === 7); //true
//possible usage like it was a primitive.
for(var i=lo.from; i<lo.to; ++i){
console.log(i, i == avg); //take a look at the first output ;)
}
//but if you want to use some of the prototype methods
//(like the correct toString()-method on Numbers, or substr on Strings)
//make sure that you have a proper primitive!
var out = avg.toFixed(3);
//will fail since this object doesn't provide the prototype-methods of Number
//+avg does the same as Number(avg)
var out = (+avg).toFixed(3); //will succeed
身份
var def = { foo: 42 };
var obj = Enum({
a: 13,
b: 13,
c: 13,
obj1: def,
obj2: def
});
//although all three have/represent the same value, they ain't the same
var v = obj.a;
console.log("testing a", v === obj.a, v === obj.b, v===obj.c); //true, false, false
var v = obj.b;
console.log("testing a", v === obj.a, v === obj.b, v===obj.c); //false, true, false
var v = obj.c;
console.log("testing a", v === obj.a, v === obj.b, v===obj.c); //false, false, true
console.log("comparing objects", obj.obj1 === obj.obj2); //false
console.log("comparing property foo", obj.obj1.foo === obj.obj2.foo); //true
//same for the values provided by the factory-functions:
console.log("compare two calls with the same args:");
console.log("Barcode.Other() === Barcode.Other()", Barcode.Other() === Barcode.Other());
//will fail, since the factory doesn't cache,
//every call creates a new Object instance.
//if you need to check wether they are equal, write a function that does that.
extendProto
//your chance to extend the prototype of each subordinated entry in the enum
//maybe you want to add some method from some other prototype
//like String.prototype or iterator-methods, or a method for equality-checking, ...
var Barcode = Enum("Barcode", {/* factories */}, function(proto, key, value){
var _barcode = this;
//so you can access the enum in closures, without the need for a "global" variable.
//but if you mess around with this, you are the one to debug the Errors you produce.
//this function is executed right after the prototpe-object for this enum-entry is created
//and before any further modification.
//neither this particular entry, nor the enum itself are done yet, so don't mess around with them.
//the only purpose of this method is to provide you a hook
//to add further properties to the proto-object
//aou can also return an object with properties to add to the proto-object.
//these properties will be added as configurable and writable but not enumerable.
//and no getter or setter. If you need more control, feel free to modify proto on you own.
return {
isBarcode: function(){
return this instanceof _barcode;
}
}
});
//OR you can define it for every single property,
//so you don't have to switch on the different properties in one huge function
var Barcode = Enum("Barcode", {/* factories */}, {
"UPCA": function(proto, key, value){
//same behaviour as the universal function
//but will be executed only for the proto of UPCA
var _barcode = this; //aka Barcode in this case
var AP = [];
return {
//copy map and indexOf from the Array prototype
map: AP.map,
indexOf: AP.indexOf,
//and add a custom toString and clone-method to the prototype
toString: function(){
return "UPCA[ "+AP.join.call(this, ", ")+" ]";
},
clone: function(){
return _barcode.UPCA.apply(null, this);
}
};
},
//OR
"QRCode": {
//or simply define an object that contains the properties/methods
//that should be added to the proto of QRCode
//again configurable and writable but not enumerable
substr: String.prototype.substr,
substring: String.prototype.substring,
charAt: String.prototype.charAt,
charCodeAt: String.prototype.charCodeAt
}
});
//mixin-functions and objects can be mixed
您可以为此使用 discriminated union。此示例使用 Typescript,但概念与 Javascript 类似,只是没有类型安全。
interface UPCA {
kind: "UPCA";
numberSystem: number;
manufacturer: number;
item: number;
checkDigit: number;
}
interface QRCode {
kind: "QRCode";
data: string;
}
interface Other {
kind: "Other";
}
type Barcode = UPCA | QRCode | Other;
然后您可以切换 Barcode
的值并在检查区分后访问关联的值。
function printBarcode(barcode: Barcode) {
switch (barcode.kind) {
case "UPCA":
console.log(`UPCA: ${barcode.numberSystem}-${barcode.manufacturer}`);
break;
case "QRCode":
console.log(`QRCode: ${barcode.data}`);
break;
case "Other":
console.log("Other barcode");
break;
}
}
Swift 语言具有出色的枚举支持。不仅可以用 case 定义标准枚举,而且 case 可以有可选值 "associated to them."
例如,取自 Swift 文档:
enum Barcode {
case UPCA(Int, Int, Int, Int)
case QRCode(String)
case Other
}
这样就可以通过传入一个值来创建条形码枚举,如下所示:
var productBarcode = Barcode.UPCA(8, 85909, 51226, 3)
以及稍后 productBarcode
上的 switch
以检索关联值(int
的元组)。
我一直在尝试在 JavaScript(尤其是 ES5)中实现这种 enum
系统,但我碰壁了。构建枚举系统的最佳方式是什么,尤其是具有关联值的系统?
枚举在我所知道的大多数语言中的工作方式并不完全相同。通常它们更像是一种将值键入为这些状态之一的方法。就像从一组可能的值中选择一个值一样。并确保这样做的类型安全,这与普通整数不同。
你在代码中发布的内容,我会调用带有工厂方法的普通对象。
由于语言不支持它们,因此您必须以尽可能满足您需求的方式实现它们。所以总结一下你期望的行为。
同时,基于我在 swift 枚举中找到的描述的实现。希望它接近您的期望:
var odp = {
ENUMERABLE: 4,
//two helper with Object.defineProperty.
value: function(obj, prop, v, flags){
this.configurable = Boolean(flags & odp.CONFIGURABLE);
this.writable = Boolean(flags & odp.WRITABLE);
this.enumerable = Boolean(flags & odp.ENUMERABLE);
this.value = v;
Object.defineProperty(obj, prop, this);
this.value = null; //v may be a function or an object: remove the reference
return obj;
}.bind({ //caching the basic definition
value: null,
configurable: false,
writable: false,
enumerable: false
}),
accessor: function(obj, prop, getter, setter){
this.get = getter || undefined;
this.set = setter || undefined;
Object.defineProperty(obj, prop, this);
this.get = null;
this.set = null;
return obj;
}.bind({ get: null, set: null })
}
//make these values immutable
odp.value(odp, "CONFIGURABLE", 1, odp.ENUMERABLE);
odp.value(odp, "WRITABLE", 2, odp.ENUMERABLE);
odp.value(odp, "ENUMERABLE", 4, odp.ENUMERABLE);
//Policy:
//1. I don't f*** care wether the keys on the definition are own or inherited keys.
//since you pass them to me, I suppose you want me to process them.
//2. If i find some undefined-value i ignore it, as if it wasn't there.
//use null to represent some "empty" value
//name and extendProto are optional
function Enum(name, description, extendProto){
var n = name, d = description, xp=extendProto;
if(n && typeof n === "object") xp=d, d = n, n = null;
var xpf = typeof xp === "function" && xp;
var xpo = typeof xp === "object" && xp;
function type(){
throw new Error("enums are not supposed to be created manually");
}
//abusing filter() as forEach()
//removing the keys that are undefined in the same step.
var keys = Object.keys(d).filter(function(key){
var val = d[key];
if(val === undefined) return false;
var proto = Object.create(type.prototype);
//your chance to extend the particular prototype with further properties
//like adding the prototype-methods of some other type
var props = xpf || xpo && xpo[key];
if(typeof props === "function")
props = props.call(type, proto, key, val);
if(props && typeof props === "object" && props !== proto && props !== val){
var flags = odp.CONFIGURABLE+odp.WRITABLE;
for(var k in props) props[k]===undefined || odp.value(proto, k, props[k], flags);
if("length" in props) odp.value(props, "length", props.length, flags);
}
if(typeof val === "function"){
//a factory and typedefinition at the same type
//call this function to create a new object of the type of this enum
//and of the type of this function at the same time
type[key] = function(){
var me = Object.create(proto);
var props = val.apply(me, arguments);
if(props && typeof props === "object" && props !== me){
for(var k in props) props[k]===undefined || odp.value(me, k, props[k], odp.ENUMERABLE);
if("length" in props) odp.value(me, "length", props.length);
}
return me;
}
//fix the fn.length-property for this factory
odp.value(type[key], "length", val.length, odp.CONFIGURABLE);
//change the name of this factory
odp.value(type[key], "name", (n||"enum")+"{ "+key+" }" || key, odp.CONFIGURABLE);
type[key].prototype = proto;
odp.value(proto, "constructor", type[key], odp.CONFIGURABLE);
}else if(val && typeof val === "object"){
for(var k in val) val[k] === undefined || odp.value(proto, k, val[k]);
if("length" in val) odp.value(proto, "length", val.length);
type[key] = proto;
}else{
//an object of the type of this enum that wraps the primitive
//a bit like the String or Number or Boolean Classes
//so remember, when dealing with this kind of values,
//you don't deal with actual primitives
odp.value(proto, "valueOf", function(){ return val; });
type[key] = proto;
}
return true;
});
odp.value(type, "name", n || "enum[ " + keys.join(", ") + " ]", odp.CONFIGURABLE);
Object.freeze(type);
return type;
}
注意,此代码可能需要进一步修改。示例:
工厂
function uint(v){ return v>>>0 }
var Barcode = Enum("Barcode", {
QRCode: function(string){
//this refers to an object of both types, Barcode and Barcode.QRCode
//aou can modify it as you wish
odp.value(this, "valueOf", function(){ return string }, true);
},
UPCA: function(a,b,c,d){
//you can also return an object with the properties you want to add
//and Arrays, ...
return [
uint(a),
uint(b),
uint(c),
uint(d)
];
//but beware, this doesn't add the Array.prototype-methods!!!
//event this would work, and be processed like an Array
return arguments;
},
Other: function(properties){
return properties; //some sugar
}
});
var productBarcode = Barcode.UPCA(8, 85909, 51226, 3);
console.log("productBarcode is Barcode:", productBarcode instanceof Barcode); //true
console.log("productBarcode is Barcode.UPCA:", productBarcode instanceof Barcode.UPCA); //true
console.log("productBarcode is Barcode.Other:", productBarcode instanceof Barcode.Other); //false
console.log("accessing values: ", productBarcode[0], productBarcode[1], productBarcode[2], productBarcode[3], productBarcode.length);
Array.prototype.forEach.call(productBarcode, function(value, index){
console.log("index:", index, " value:", value);
});
对象和基元
var indices = Enum({
lo: { from: 0, to: 13 },
hi: { from: 14, to: 42 },
avg: 7
});
var lo = indices.lo;
console.log("lo is a valid index", lo instanceof indices);
console.log("lo is indices.lo", lo === indices.lo);
//indices.lo always references the same Object
//no function-call, no getter!
var avg = indices.avg; //beware, this is no primitive, it is wrapped
console.log("avg is a valid index", avg instanceof indices);
console.log("comparison against primitives:");
console.log(" - typesafe", avg === 7); //false, since avg is wrapped!!!
console.log(" - loose", avg == 7); //true
console.log(" - typecast+typesafe", Number(avg) === 7); //true
//possible usage like it was a primitive.
for(var i=lo.from; i<lo.to; ++i){
console.log(i, i == avg); //take a look at the first output ;)
}
//but if you want to use some of the prototype methods
//(like the correct toString()-method on Numbers, or substr on Strings)
//make sure that you have a proper primitive!
var out = avg.toFixed(3);
//will fail since this object doesn't provide the prototype-methods of Number
//+avg does the same as Number(avg)
var out = (+avg).toFixed(3); //will succeed
身份
var def = { foo: 42 };
var obj = Enum({
a: 13,
b: 13,
c: 13,
obj1: def,
obj2: def
});
//although all three have/represent the same value, they ain't the same
var v = obj.a;
console.log("testing a", v === obj.a, v === obj.b, v===obj.c); //true, false, false
var v = obj.b;
console.log("testing a", v === obj.a, v === obj.b, v===obj.c); //false, true, false
var v = obj.c;
console.log("testing a", v === obj.a, v === obj.b, v===obj.c); //false, false, true
console.log("comparing objects", obj.obj1 === obj.obj2); //false
console.log("comparing property foo", obj.obj1.foo === obj.obj2.foo); //true
//same for the values provided by the factory-functions:
console.log("compare two calls with the same args:");
console.log("Barcode.Other() === Barcode.Other()", Barcode.Other() === Barcode.Other());
//will fail, since the factory doesn't cache,
//every call creates a new Object instance.
//if you need to check wether they are equal, write a function that does that.
extendProto
//your chance to extend the prototype of each subordinated entry in the enum
//maybe you want to add some method from some other prototype
//like String.prototype or iterator-methods, or a method for equality-checking, ...
var Barcode = Enum("Barcode", {/* factories */}, function(proto, key, value){
var _barcode = this;
//so you can access the enum in closures, without the need for a "global" variable.
//but if you mess around with this, you are the one to debug the Errors you produce.
//this function is executed right after the prototpe-object for this enum-entry is created
//and before any further modification.
//neither this particular entry, nor the enum itself are done yet, so don't mess around with them.
//the only purpose of this method is to provide you a hook
//to add further properties to the proto-object
//aou can also return an object with properties to add to the proto-object.
//these properties will be added as configurable and writable but not enumerable.
//and no getter or setter. If you need more control, feel free to modify proto on you own.
return {
isBarcode: function(){
return this instanceof _barcode;
}
}
});
//OR you can define it for every single property,
//so you don't have to switch on the different properties in one huge function
var Barcode = Enum("Barcode", {/* factories */}, {
"UPCA": function(proto, key, value){
//same behaviour as the universal function
//but will be executed only for the proto of UPCA
var _barcode = this; //aka Barcode in this case
var AP = [];
return {
//copy map and indexOf from the Array prototype
map: AP.map,
indexOf: AP.indexOf,
//and add a custom toString and clone-method to the prototype
toString: function(){
return "UPCA[ "+AP.join.call(this, ", ")+" ]";
},
clone: function(){
return _barcode.UPCA.apply(null, this);
}
};
},
//OR
"QRCode": {
//or simply define an object that contains the properties/methods
//that should be added to the proto of QRCode
//again configurable and writable but not enumerable
substr: String.prototype.substr,
substring: String.prototype.substring,
charAt: String.prototype.charAt,
charCodeAt: String.prototype.charCodeAt
}
});
//mixin-functions and objects can be mixed
您可以为此使用 discriminated union。此示例使用 Typescript,但概念与 Javascript 类似,只是没有类型安全。
interface UPCA {
kind: "UPCA";
numberSystem: number;
manufacturer: number;
item: number;
checkDigit: number;
}
interface QRCode {
kind: "QRCode";
data: string;
}
interface Other {
kind: "Other";
}
type Barcode = UPCA | QRCode | Other;
然后您可以切换 Barcode
的值并在检查区分后访问关联的值。
function printBarcode(barcode: Barcode) {
switch (barcode.kind) {
case "UPCA":
console.log(`UPCA: ${barcode.numberSystem}-${barcode.manufacturer}`);
break;
case "QRCode":
console.log(`QRCode: ${barcode.data}`);
break;
case "Other":
console.log("Other barcode");
break;
}
}