2016-01-30 59 views
7

Swift語言有一個夢幻般的枚舉支持。不僅可以定義具有案例的標準枚舉,而且案例可以具有「與其相關聯」的可選值。如何在JavaScript中實現類似Swift的枚舉與關聯值?

例如,從夫特文檔採取:

enum Barcode { 
    case UPCA(Int, Int, Int, Int) 
    case QRCode(String) 
    case Other 
} 

,使得人們可以通過使在一個值,像這樣創建條形碼枚舉:

var productBarcode = Barcode.UPCA(8, 85909, 51226, 3)

並且還switchproductBarcode在稍後的日期檢索關聯值(一個int s的元組)。


我一直在試圖實現這種enum系統在JavaScript(ES5,特別是),但我擊中牆壁。構建一個枚舉系統的最佳方式是什麼,特別是具有相關值的一個?

+1

男人,我一直有麻煩做同樣的事情。我在Swift中喜歡它們,但在JavaScript中需要它們! – boztalay

回答

3

這不正是枚舉在我知道的大多數語言中的工作方式。通常它們更像是一種將值作爲這些狀態之一輸入的方式。就像從一組可能的值中選擇一個值一樣。爲了確保類型安全性,與普通整數不同。

你在代碼中發佈了什麼,我會用工廠方法調用一個普通的對象。

由於它們不受語言支持,所以必須以儘可能好的方式滿足您的需求。所以總結一下你期望的行爲。

同時基於我在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