外部声明的用途
外部声明用于告知 Closure 编译器在高级编译期间不应重命名的符号的名称。 之所以称为外部变量,是因为这些符号通常由编译范围之外的代码(例如原生代码或第三方库)定义。因此,外部声明通常也具有类型注释,以便 Closure 编译器可以对您对这些符号的使用进行类型检查。
一般来说,最好将外部声明视为实现者与某些已编译代码的使用者之间的 API 契约。外部声明定义了实现者承诺提供的功能,以及使用方可以依赖的功能。双方都需要一份合同副本。
外部声明类似于其他语言中的头文件。
外部声明语法
外部声明文件看起来很像针对 Closure 编译器的带注释的常规 JavaScript 文件。主要区别在于,它们的内容永远不会作为编译输出的一部分进行打印,因此这些值没有任何意义,只有名称和类型有意义。
下面是一个简单库的 externs 文件示例。
// The `@externs` annotation is the best way to indicate a file contains externs. /** * @fileoverview Public API of my_math.js. * @externs */ // Externs often declare global namespaces. const myMath = {}; // Externs can declare functions, most importantly their names. /** * @param {number} x * @param {number} y * @return {!myMath.DivResult} */ myMath.div = function(x, y) {}; // Note the empty body. // Externs can contain type declarations, such as classes and interfaces. /** The result of an integer division. */ myMath.DivResult = class { // Constructors are special; member fields can be declared in their bodies. constructor() { /** @type {number} */ this.quotient; /** @type {number} */ this.remainder; } // Methods can be declared as usual; their bodies are meaningless though. /** @return {!Array<number>} */ toPair() {} }; // Fields and methods can also be declared using prototype notation. /** * @override * @param {number=} radix */ myMath.DivResult.prototype.toString = function(radix) {};
--externs
标志
一般来说,@externs
注释是告知编译器文件包含外部声明的最佳方式。此类文件可使用 --js
命令行标志作为常规源文件包含在内,
不过,还有一种较旧的方式来指定 externs 文件。--externs
命令行标志可用于显式传递 externs 文件。不建议使用此方法。
使用外部声明
上述外部声明可按如下方式使用。
/** * @fileoverview Do some math. */ /** * @param {number} x * @param {number} y * @return {number} */ export function greatestCommonDivisor(x, y) { while (y != 0) { const temp = y; // `myMath` is a global, it and `myMath.div` are never renamed. const result = myMath.div(x, y); // `remainder` is also never renamed on instances of `DivResult`. y = result.remainder; x = temp; } return x; }
导出目的
导出是另一种在编译后为符号提供一致名称的机制。它们不如外部声明有用,而且往往令人困惑。除了简单的情况外,最好避免使用它们。
导出依赖于 Closure 编译器不修改字符串字面量这一事实。 通过将对象分配给使用字面量命名的属性,即使在编译后,该对象仍可通过该属性名称访问。
下面是一个简单的示例。
/** * @fileoverview Do some math. */ // Note that the concept of module exports is totally unrelated. /** @return {number} */ export function myFunction() { return 5; } // This assignment ensures `myFunctionAlias` will be a global alias exposing `myFunction`, // even after compilation. window['myFunctionAlias'] = myFunction;
如果您使用的是 Closure 库,也可以使用 goog.exportSymbol
和 goog.exportProperty
函数声明导出。
如需了解详情,请参阅这些函数的 Closure 库文档。不过,请注意,它们有特殊的编译器支持,并且在编译后的输出中会完全转换。
导出问题
导出与外部声明不同,前者仅为消费者创建一个可供引用的公开别名。在编译后的代码中,导出的符号仍会被重命名。因此,导出的符号必须是常量,因为在代码中重新分配它们会导致公开的别名指向错误的内容。
在重命名方面,这种细微差别对于导出的实例属性尤其复杂。
从理论上讲,与外部声明相比,导出可以实现更小的代码大小,因为长名称仍然可以在代码中更改为短名称。在实践中,这些改进通常非常细微,不足以证明导出功能造成的混乱是合理的。
导出也不会像外部声明那样为消费者提供 API 以供遵循。与导出相比,外部声明会记录您打算公开的符号及其类型,并为您提供一个添加使用信息的位置。此外,如果您的消费者也使用 Closure Compiler,他们将需要外部声明才能进行编译。