首页 文章

不同的枚举变体如何在TypeScript中工作?

提问于
浏览
71

TypeScript有许多不同的方法来定义枚举:

enum Alpha { X, Y, Z }
const enum Beta { X, Y, Z }
declare enum Gamma { X, Y, Z }
declare const enum Delta { X, Y, Z }

如果我在运行时尝试使用 Gamma 中的值,则会收到错误,因为 Gamma 未定义,但 DeltaAlpha 不是这种情况? constdeclare 在声明中的含义是什么?

还有一个 preserveConstEnums 编译器标志 - 这与这些标志如何相互作用?

2 回答

  • 176

    您需要注意TypeScript中的枚举有四个不同的方面 . 首先,一些定义:

    “查找对象”

    如果你写这个枚举:

    enum Foo { X, Y }
    

    TypeScript将发出以下对象:

    var Foo;
    (function (Foo) {
        Foo[Foo["X"] = 0] = "X";
        Foo[Foo["Y"] = 1] = "Y";
    })(Foo || (Foo = {}));
    

    我将其称为查找对象 . 它的目的是双重的:用作从字符串到数字的映射,例如写 Foo.XFoo['X'] 时,用作从数字到字符串的映射 . 反向映射对于调试或记录目的很有用 - 您通常会使用值 01 并希望获得相应的字符串 "X""Y" .

    “声明”或“环境”

    在TypeScript中,您可以"declare"编译器应该知道的事情,但实际上不会为其发出代码 . 当你有像jQuery这样的库定义一些你想要类型信息但不需要编译器创建的代码的对象(例如 $ )时,这很有用 . 规范和其他文档指的是这样做的声明在"ambient"上下文中;重要的是要注意 .d.ts 文件中的所有声明都是"ambient"(要么需要显式的 declare 修饰符,要么隐式地使用它,具体取决于声明类型) .

    “内联”

    出于性能和代码大小的原因,通常最好在编译时将枚举成员的引用替换为其数字等效项:

    enum Foo { X = 4 }
    var y = Foo.X; // emits "var y = 4";
    

    该规范称之为替换,我将其称为内联,因为它听起来更酷 . 有时您不希望枚举枚举成员,例如因为枚举值可能会在API的未来版本中发生变化 .


    枚举,它们如何运作?

    让我们通过枚举的每个方面来打破这个 . 不幸的是,这四个部分中的每一部分都将引用所有其他部分的术语,因此您可能需要不止一次地阅读这整个部分 .

    计算与非计算(常数)

    枚举成员可以是否可以计算 . 规范将非计算成员调用为常量,但我将其称为非计算,以避免与const混淆 .

    计算的枚举成员是在编译时不知道其值的成员 . 当然,不能内联对计算成员的引用 . 相反,非计算枚举成员的值在编译时是已知的 . 始终内联对非计算成员的引用 .

    哪些枚举成员是计算的,哪些是非计算的?首先,顾名思义, const 枚举的所有成员都是常量(即非计算的) . 对于非常量枚举,它取决于您是在查看环境(声明)枚举还是非环境枚举 .

    declare enum (即环境枚举)的成员当且仅当它具有初始化器时才是常量 . 否则,计算它 . 请注意,在 declare enum 中,只允许使用数字初始值设定项 . 例:

    declare enum Foo {
        X, // Computed
        Y = 2, // Non-computed
        Z, // Computed! Not 3! Careful!
        Q = 1 + 1 // Error
    }
    

    最后,始终认为非声明非const枚举的成员是计算的 . 但是,如果它们在编译时可计算,则它们的初始化表达式会减少到常量 . 这意味着非const枚举成员永远不会内联(在TypeScript 1.5中更改了此行为,请参阅底部的“TypeScript中的更改”)

    const vs非const

    const

    枚举声明可以具有 const 修饰符 . 如果枚举是 const ,则所有对其成员的引用都是内联的 .

    const enum Foo { A = 4 }
    var x = Foo.A; // emitted as "var x = 4;", always
    

    const枚举在编译时不会生成查找对象 . 因此,在上述代码中引用 Foo 是错误的,除非作为成员引用的一部分 . 运行时不会出现 Foo 对象 .

    非const

    如果枚举声明没有 const 修饰符,则仅当成员未计算时才会内联对其成员的引用 . 非const,非声明枚举将生成查找对象 .

    declare(ambient)vs non-declare

    一个重要的前言是TypeScript中的 declare 具有非常特定的含义:该对象存在于其他地方 . 它用于描述现有对象 . 使用 declare 定义对象不要't actually exist can have bad consequences; we'以后再探讨 .

    宣布

    declare enum 不会发出查找对象 . 如果计算了这些成员,则会内联对其成员的引用(参见上面的计算与非计算) .

    重要的是要注意允许对 declare enum 的其他形式的引用,例如此代码不是编译错误,但会在运行时失败:

    // Note: Assume no other file has actually created a Foo var at runtime
    declare enum Foo { Bar } 
    var s = 'Bar';
    var b = Foo[s]; // Fails
    

    此错误属于"Don't lie to the compiler"类别 . 如果在运行时没有名为 Foo 的对象,请不要写 declare enum Foo

    declare const enumconst enum 没有区别,除了--preserveConstEnums(见下文) .

    非申报

    如果非声明枚举不是 const ,则它会生成查找对象 . 上面描述了内联 .

    --preserveConstEnums标志

    此标志只有一个效果:非声明const枚举将发出查找对象 . 内联不受影响 . 这对调试很有用 .


    常见错误

    最常见的错误是当常规 enumconst enum 更合适时使用 declare enum . 一个常见的形式是:

    module MyModule {
        // Claiming this enum exists with 'declare', but it doesn't...
        export declare enum Lies {
            Foo = 0,
            Bar = 1     
        }
        var x = Lies.Foo; // Depend on inlining
    }
    
    module SomeOtherCode {
        // x ends up as 'undefined' at runtime
        import x = MyModule.Lies;
    
        // Try to use lookup object, which ought to exist
        // runtime error, canot read property 0 of undefined
        console.log(x[x.Foo]);
    }
    

    记住黄金法则:从来没有 declare 实际上不存在的东西 . 如果您总是想要内联,请使用 const enum ;如果需要查找对象,请使用 enum .


    TypeScript中的更改

    在TypeScript 1.4和1.5之间,行为发生了变化(请参阅https://github.com/Microsoft/TypeScript/issues/2183),以使非声明非const枚举的所有成员都被视为已计算,即使它们已使用文字显式初始化 . 可以这么说,"unsplit the baby",使内联行为更可预测,更清晰地将 const enum 的概念与常规 enum 分开 . 在此更改之前,非const enums的非计算成员被更加积极地内联 .

  • 10

    这里有一些事情发生 . 让我们逐个进行 .

    枚举

    enum Cheese { Brie, Cheddar }
    

    首先,一个普通的老枚举 . 编译为JavaScript时,这将发出一个查找表 .

    查找表如下所示:

    var Cheese;
    (function (Cheese) {
        Cheese[Cheese["Brie"] = 0] = "Brie";
        Cheese[Cheese["Cheddar"] = 1] = "Cheddar";
    })(Cheese || (Cheese = {}));
    

    然后当你在TypeScript中有 Cheese.Brie 时,它会在JavaScript中发出 Cheese.Brie ,其值为0. Cheese[0] 发出 Cheese[0] 并实际计算为 "Brie" .

    const enum

    const enum Bread { Rye, Wheat }
    

    实际上没有为此发出代码!它的值是内联的 . 以下内容在JavaScript中发出值0:

    Bread.Rye
    Bread['Rye']
    

    const enum s'内联可能因性能原因而有用 .

    但是 Bread[0] 怎么样?这将在运行时出错,您的编译器应该捕获它 . 那里有's no lookup table and the compiler doesn'吨内联 .

    请注意,在上面的情况中, - prepareConstEnums标志将导致Bread发出查找表 . 它的 Value 仍然会被内联 .

    声明枚举

    declare 的其他用法一样, declare 不会发出代码,并且您希望在其他位置定义实际代码 . 这不会发出查找表:

    declare enum Wine { Red, Wine }
    

    Wine.Red 在JavaScript中发出 Wine.Red ,但除非您在别处定义了't be any Wine lookup table to reference so it',否则会出现错误 .

    declare const enum

    这不会发出查找表:

    declare const enum Fruit { Apple, Pear }
    

    但它确实内联! Fruit.Apple 发出0.但是 Fruit[0] 将在运行时错误输出,因为它没有查找表's not inlined and there' .

    我在this操场上写了这篇文章 . 我建议在那里玩,以了解哪个TypeScript发出哪个JavaScript .

相关问题