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

How do the different enum variants work in TypeScript?

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 未定义,但 Delta 或 [=14 并非如此=]? constdeclare 在此处的声明中是什么意思?

还有一个 preserveConstEnums 编译器标志 -- 它如何与它们交互?

您需要了解 TypeScript 中枚举的四个不同方面。首先,一些定义:

"lookup object"

如果你写这个枚举:

enum Foo { X, Y }

TypeScript 将发出以下对象:

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

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

"declare" 或“环境

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

"inlining"

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

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

规范称此为 substitution,我将其称为 inlining,因为它听起来更酷。有时您 想要内联枚举成员,例如因为枚举值可能会在 API.

的未来版本中发生变化

枚举,它们是如何工作的?

让我们按枚举的各个方面进行分解。不幸的是,这四个部分中的每一部分都将引用所有其他部分的术语,因此您可能需要多次阅读整个内容。

计算与非计算(常量)

枚举成员可以计算,也可以不计算。规范调用非计算成员 constant,但我将它们称为 non-computed 以避免与 const[= 混淆163=].

A computed 枚举成员的值在编译时未知。当然,不能内联对计算成员的引用。相反,非计算 枚举成员曾经在编译时知道其值。对非计算成员的引用总是内联的。

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

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

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

最后,非声明非常量枚举的成员始终被视为已计算。但是,如果它们在编译时可计算,它们的初始化表达式将简化为常量。这意味着永远不会内联非常量枚举成员(此行为在 TypeScript 1.5 中发生了变化,请参阅底部的 "Changes in TypeScript")

常量与非常量

常量

枚举声明可以有 const 修饰符。如果枚举是 const 所有 内联其成员的引用。

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

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

非常量

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

声明(环境)与不声明

一个重要的前言是declare在TypeScript中有一个非常具体的含义:This object exists somewhere else。它用于描述 现有 对象。使用 declare 定义实际上不存在的对象可能会产生不良后果;我们稍后会探讨这些。

声明

A 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!

A declare const enumconst enum 没有区别,除了 --preserveConstEnums 的情况(见下文)。

未申报

如果非声明枚举不是 const,则会生成查找对象。内联如上所述。

--preserveConstEnums 标志

这个标志只有一个作用:非声明常量枚举将发出一个查找对象。内联不受影响。这对调试很有用。


常见错误

最常见的错误是在 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),使非声明非常量枚举的所有成员都被视为已计算,即使它们已被显式初始化用文字。这 "unsplit the baby",可以这么说,使内联行为更可预测,并且更清楚地将 const enum 的概念与常规 enum 的概念分开。在此更改之前,非常量枚举的非计算成员被更积极地内联。

这里发生了一些事情。让我们逐一分析。

枚举

enum Cheese { Brie, Cheddar }

首先,一个普通的旧枚举。当编译为 JavaScript 时,这将发出查找 table.

查找 table 如下所示:

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 Bread { Rye, Wheat }

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

Bread.Rye
Bread['Rye']

const enum出于性能原因,内联可能很有用。

但是 Bread[0] 呢?这将在运行时出错,您的编译器应该能够捕捉到它。没有查找 table 并且编译器不会在此处内联。

请注意,在上述情况下,--preserveConstEnums 标志将导致 Bread 发出查找 table。它的值仍然会被内联。

声明枚举

declare 的其他用途一样,declare 不发出任何代码,并希望您在别处定义实际代码。这不发出查找 table:

declare enum Wine { Red, Wine }

Wine.Red 在 JavaScript 中发出 Wine.Red,但不会有任何 Wine 查找 table 可供参考,因此这是一个错误,除非您在别处定义了它.

声明常量枚举

这不会发出任何查找 table:

declare const enum Fruit { Apple, Pear }

但它确实是内联的! Fruit.Apple 发出 0。但是 Fruit[0] 将在运行时再次出错,因为它没有内联并且没有查找 table.

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