不同的枚举变体如何在 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 并非如此=]? const
或 declare
在此处的声明中是什么意思?
还有一个 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 = {}));
我将其称为查找对象。它的目的是双重的:作为从 strings 到 numbers 的映射,例如当写入 Foo.X
或 Foo['X']
时,并用作从 numbers 到 strings 的映射。反向映射对于调试或记录目的很有用——您通常会有值 0
或 1
并希望获得相应的字符串 "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 enum
与 const enum
没有区别,除了 --preserveConstEnums 的情况(见下文)。
未申报
如果非声明枚举不是 const
,则会生成查找对象。内联如上所述。
--preserveConstEnums 标志
这个标志只有一个作用:非声明常量枚举将发出一个查找对象。内联不受影响。这对调试很有用。
常见错误
最常见的错误是在 enum
或 const 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.
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 并非如此=]? const
或 declare
在此处的声明中是什么意思?
还有一个 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 = {}));
我将其称为查找对象。它的目的是双重的:作为从 strings 到 numbers 的映射,例如当写入 Foo.X
或 Foo['X']
时,并用作从 numbers 到 strings 的映射。反向映射对于调试或记录目的很有用——您通常会有值 0
或 1
并希望获得相应的字符串 "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 enum
与 const enum
没有区别,除了 --preserveConstEnums 的情况(见下文)。
未申报
如果非声明枚举不是 const
,则会生成查找对象。内联如上所述。
--preserveConstEnums 标志
这个标志只有一个作用:非声明常量枚举将发出一个查找对象。内联不受影响。这对调试很有用。
常见错误
最常见的错误是在 enum
或 const 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.