Extern 与 Exports

外部声明的用途

外部声明用于告知 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.exportSymbolgoog.exportProperty 函数声明导出。

如需了解详情,请参阅这些函数的 Closure 库文档。不过,请注意,它们有特殊的编译器支持,并且在编译后的输出中会完全转换。

导出问题

导出与外部声明不同,前者仅为消费者创建一个可供引用的公开别名。在编译后的代码中,导出的符号仍会被重命名。因此,导出的符号必须是常量,因为在代码中重新分配它们会导致公开的别名指向错误的内容。

在重命名方面,这种细微差别对于导出的实例属性尤其复杂。

从理论上讲,与外部声明相比,导出可以实现更小的代码大小,因为长名称仍然可以在代码中更改为短名称。在实践中,这些改进通常非常细微,不足以证明导出功能造成的混乱是合理的。

导出也不会像外部声明那样为消费者提供 API 以供遵循。与导出相比,外部声明会记录您打算公开的符号及其类型,并为您提供一个添加使用信息的位置。此外,如果您的消费者也使用 Closure Compiler,他们将需要外部声明才能进行编译。