Google的Javascript风格指南
3 源文件结构
所有新的源文件应为goog.module(包含goog.module所调用的文件)或者ECMAScript(ES)模块(使用import和export语句)。文件按顺序包括以下内容:
- 许可证或者版权信息(如果有);
- @fileoverview,JSDoc(如果存在);
- goog.module语句(如果是goog.module文件);
- ES导入语句(如果是ES模块);
- goog.require和goog.requireType语句;
- 文件的实现;
除了文件的实现(每行前面可能会出现1或者2个空行)外,实际上每个节之间存在空行分割。
3.1 许可证或者版权信息(如果有)
如果许可证或者版权信息属于文件,则它属于此处。
3.2 @fileoverview JSDoc (如果有)
关于格式化规则,可参见7.5 顶级/文件级注释。
3.3 goog.module语句
所有goog.module文件必须在一行上仅声明一个goog.module名称:不得包装包含goog.module声明的行,因此也称为80列限制的例外。
goog.module的整个参数是定义名称空间的内容,它是程序包名称(一个标识符,反映了代码所在的目录结构的片段)加上(可选)它定义到末尾的主类/枚举/接口。
示例:
goog.module('search.urlHistory.UrlHistoryService');
3.3.1 层次结构
模块名称空间永远不能被命名为另一个模块名称空间的直接子代。
不允许:
goog.module('foo.bar'); // 'foo.bar.qux' would be fine, though
goog.module('foo.bar.baz');
目录层次结构反映了名称空间的层次结构。因此,嵌套较深的子级是较高父目录的子目录。请注意,这意味着“父”名称空间组的所有者必须知道所有子名称空间,因为它们存在于同一目录中。
3.3.2 goog.module.declareLegacyNamespace
在单个goog.module语句之后,可以选择调用goog.module.declareLegacyNamespace();但尽可能避免使用goog.module.declareLegacyNamespace()。
示例:
goog.module('my.test.helpers');
goog.module.declareLegacyNamespace();
goog.setTestOnly();
goog.module.declareLegacyNamespace的存在是为了简化从传统的基于对象层次结构的名称空间的过渡,但存在一些命名限制。 由于子模块名称必须在父名称空间之后创建,因此该名称不能是其他任何goog.module(例如goog.module(‘parent’)。goog.module('parent.child ');不能安全地存在,同时goog.module(‘parent’)和goog.module(‘parent.child.grandchild’)也不能安全地存在。
3.3.3 goog.module导出
类、枚举、函数、常量以及其它符号可以使用导出对象导出。导出的符号可以直接在导出对象上定义,也可以在本地声明并单独导出。仅当打算在模块外部使用符号时,才导出符号。未导出的模块本地符号不会声明为@private,也不会以下划线结尾。对于导出的符号和模块本地符号没有规定顺序。
示例:
const /** !Array<number> */ exportedArray = [1, 2, 3];
const /** !Array<number> */ moduleLocalArray = [4, 5, 6];
/** @return {number} */
function moduleLocalFunction() {
return moduleLocalArray.length;
}
/** @return {number} */
function exportedFunction() {
return moduleLocalFunction() * 2;
}
exports = {exportedArray, exportedFunction};
/** @const {number} */
exports.CONSTANT_ONE = 1;
/** @const {string} */
exports.CONSTANT_TWO = 'Another constant';
不要将导出对象注释为@const,因为编译器已经将其视为常量,如下所示。
/** @const */
exports = {exportedFunction};
3.4 ES模块
3.4.1 导入
导入语句不能换行,因此也是80列限制的例外。
3.4.1.1 导入路径
ES模块文件必须使用import语句来导入其它ES模块文件。请勿goog.require另一个ES模块。
import './sideeffects.js';
import * as goog from '../closure/goog/goog.js';
import * as parent from '../parent.js';
import {name} from './sibling.js';
3.4.1.1.1 在路径导入中的文件名扩展
.js文件扩展名在导入路径中不是可选的,必须始终包括在内。
import '../directory/file';
import '../directory/file.js';
3.4.1.2 多次导入同一文件
不要多次导入同一文件,这会使确定文件的聚合导入变得困难。
// Imports have the same path, but since it doesn't align it can be hard to see.
import {short} from './long/path/to/a/file.js';
import {aLongNameThatBreaksAlignment} from './long/path/to/a/file.js';
3.4.1.3 名称导入
3.4.1.3.1 名称模块导入
模块导入名称(import *作为名称)是从导入文件名派生的lowerCamelCase名称。
import * as fileOne from '../file-one.js';
import * as fileTwo from '../file_two.js';
import * as fileThree from '../filethree.js';
import * as libString from './lib/string.js';
import * as math from './math/math.js';
import * as vectorMath from './vector/math.js';
3.4.1.3.2 名称默认导入
默认导入名称是从导入的文件名派生的,并按照6.2 标识符类型规则的规则进行操作。
import MyClass from '../my-class.js';
import myFunction from '../my_function.js';
import SOME_CONSTANT from '../someconstant.js';
注意:通常这不会发生,因为此样式指南禁止默认导出,请参加3.4.2.1 重命名与默认导出名称与默认导出。默认导入仅用于导入不符合该样式指南的模块。
3.4.1.3.3 命名重命名导入
通常,通过命名导入(导入{name})导入的符号应保持相同的名称。 避免使用别名导入(将{SomeThing as SomeOtherThing}导入)。 建议使用模块导入(import *)或重命名导出本身来解决名称冲突。
import * as bigAnimals from './biganimals.js';
import * as domesticatedAnimals from './domesticatedanimals.js';
new bigAnimals.Cat();
new domesticatedAnimals.Cat();
如果需要重命名已命名的导入,则在结果别名中使用导入模块的文件名或路径的组件。
import {Cat as BigCat} from './biganimals.js';
import {Cat as DomesticatedCat} from './domesticatedanimals.js';
new BigCat();
new DomesticatedCat();
3.4.2 导出
仅在打算在模块外部使用符号时,才导出符号。 未导出的模块本地符号不会声明为@private,也不会以下划线结尾。 对于导出的符号和模块本地符号没有规定的顺序。
3.4.2.1 重命名与默认导出
在所有代码中使用命名的导出。 您可以将export关键字应用于声明,或使用export {name}; 句法。
不要使用默认导出(export default)。 导入模块必须为这些值命名,这可能导致模块之间的命名不一致。
// Do not use default exports:
export default class Foo { ... } // BAD!
// Use named exports:
export class Foo { ... }
// Alternate style named exports:
class Foo { ... }
export {Foo};
3.4.2.2 导出静态容器类和对象
为了命名空间,请勿导出包含静态方法和属性的容器类或者对象:
// container.js
// Bad: Container is an exported class that has only static methods and fields.
export class Container {
/** @return {number} */
static bar() {
return 1;
}
}
/** @const {number} */
Container.FOO = 1;
相反,导出单个常量和函数:
/** @return {number} */
export function bar() {
return 1;
}
export const /** number */ FOO = 1;
3.4.2.3 导出的可变性
导出的变量不得在模块初始化之外进行重新赋值。
如果需要进行重新赋值,则有其他选择,包括导出对具有可变字段的对象的恒定引用或导出可变数据的访问器功能。以下代码段不被推荐:
// Bad: both foo and mutateFoo are exported and mutated.
export let /** number */ foo = 0;
/**
* Mutates foo.
*/
export function mutateFoo() {
++foo;
}
/**
* @param {function(number): number} newMutateFoo
*/
export function setMutateFoo(newMutateFoo) {
// Exported classes and functions can be mutated!
mutateFoo = () => {
foo = newMutateFoo(foo);
};
}
以下代码段被推荐:
// Good: Rather than export the mutable variables foo and mutateFoo directly,
// instead make them module scoped and export a getter for foo and a wrapper for
// mutateFooFunc.
let /** number */ foo = 0;
let /** function(number): number */ mutateFooFunc = foo => foo + 1;
/** @return {number} */
export function getFoo() {
return foo;
}
export function mutateFoo() {
foo = mutateFooFunc(foo);
}
/** @param {function(number): number} mutateFoo */
export function setMutateFoo(mutateFoo) {
mutateFooFunc = mutateFoo;
}
3.4.2.4 export from语句
语句中的export from不能换行,因此是80列限制的例外。这也适用于export from是语法糖的情况。
export {specificName} from './other.js';
export * from './another.js';
3.4.3 ES模块中的循环依赖
即使ECMAScript规范允许这样做,也不要在ES模块之间创建循环。请注意,可以使用导入和导出语句创建循环。以下三个代码块都是不被推荐使用的:
// a.js
import './b.js';
// b.js
import './a.js';
// `export from` can cause circular dependencies too!
export {x} from './c.js';
// c.js
import './b.js';
export let x;
3.4.4 与闭包的互操作
3.4.4.1 应用goog
要应用Closure goog命名空间,请导入Closure的goog.js。
import * as goog from '../closure/goog/goog.js';
const name = goog.require('a.name');
export const CONSTANT = name.compute();
goog.js仅从全局goog中导出可在ES模块中使用的属性的子集。
3.4.4.2 ES模块中的goog.require
ES模块中的goog.require与goog.module文件中的goog.require一样。 您可以要求使用任何Closure名称空间符号(即由goog.provide或goog.module创建的符号),goog.require将返回该值。
import * as goog from '../closure/goog/goog.js';
import * as anEsModule from './anEsModule.js';
const GoogPromise = goog.require('goog.Promise');
const myNamespace = goog.require('my.namespace');
3.4.4.3 在ES模块中声明封闭模块ID
goog.declareModuleId可以在ES模块中使用,以声明类似goog.module的模块ID。 这意味着该模块ID可以是goog.required,goog.module.getd,goog.forwardDeclare’d等,就好像它是未调用goog.module.declareLegacyNamespace的goog.module一样。 它不会将模块ID创建为全局可用的JavaScript符号。
来自goog.declareModuleId的模块ID的goog.require(或goog.module.get)将始终返回模块对象(就像已导入*'d一样)。 结果,goog.declareModuleId的参数应始终以lowerCamelCaseName结尾。
注意:在ES模块中调用goog.module.declareLegacyNamespace是错误的,只能从goog.module文件中调用。 没有直接的方法将旧名称空间与ES模块关联。
goog.declareModuleId仅应用于将Closure文件升级到使用命名导出的ES模块。
import * as goog from '../closure/goog.js';
goog.declareModuleId('my.esm');
export class Class {};
3.5 goog.setTestOnly
在goog.module文件中,可以选择在goog.module语句后调用goog.setTestOnly()。
在ES模块中,可以在import语句之后可选地调用goog.setTestOnly()。
3.6 goog.require和goog.requireType语句
导入是通过goog.require和goog.requireType语句完成的。由goog.require语句导入的名称可以在代码和类型注释中使用,而由goog.requireType导入的名称只能在类型注释中使用。
goog.require和goog.requireType语句形成没有空行的连续块。该代码块遵循goog.module声明,并用一个空行分隔。 goog.require或goog.requireType的整个参数是由goog.module在单独文件中定义的名称空间。 goog.require和goog.requireType语句可能不会出现在文件中的其他任何位置。
每个goog.require或goog.requireType都分配给一个常量别名,或者分解为几个常量别名。这些别名是引用类型注释或代码中的依赖项的唯一可接受的方法。完全合格的名称空间不得在任何地方使用,除非作为goog.require或goog.requireType的参数。
例外:externs文件中声明的类型,变量和函数必须在类型注释和代码中使用其完全限定的名称。
别名必须与导入的模块名称空间的最后的点分隔组件匹配。
例外:在某些情况下,名称空间的其他组件可用于形成更长的别名。生成的别名必须保留原始标识符的大小写,以便仍可以正确标识其类型。较长的别名可以用于消除其他相同别名的歧义,或者可以大幅度提高可读性。此外,必须使用更长的别名来防止屏蔽本机类型,例如Element,Event,Error,Map和Promise(有关更完整的列表,请参见Standard Built-in Objects和Web APIs)。重命名变形的别名时,必须按照4.6.2 水平空格的要求,在冒号后面加上一个空格。
对于同一名称空间,文件不应同时包含goog.require和goog.requireType语句。如果在代码注释和类型注释中都使用了导入的名称,则应通过单个goog.require语句将其导入。
如果仅出于副作用导入模块,则该调用必须是goog.require(而不是goog.requireType),并且可以省略分配。需要注释以解释为什么这样做并禁止编译器警告。
根据以下规则对行进行排序:首先要求所有名称在左侧,并按这些名称的字母顺序进行排序。然后需要进行销毁,并再次按左侧名称进行排序。最后,任何要求调用都是独立的(通常是针对副作用而导入的模块)。
无需记住此命令并手动执行。可以依靠IDE来报告未正确排序的要求。
如果较长的别名或者模块名称会导致行超过80列的限制,则不能将其换行:require行是80列的限制的例外。
示例:
// Standard alias style.
const MyClass = goog.require('some.package.MyClass');
const MyType = goog.requireType('some.package.MyType');
// Namespace-based alias used to disambiguate.
const NsMyClass = goog.require('other.ns.MyClass');
// Namespace-based alias used to prevent masking native type.
const RendererElement = goog.require('web.renderer.Element');
// Out of sequence namespace-based aliases used to improve readability.
// Also, require lines longer than 80 columns must not be wrapped.
const SomeDataStructureModel = goog.requireType('identical.package.identifiers.models.SomeDataStructure');
const SomeDataStructureProto = goog.require('proto.identical.package.identifiers.SomeDataStructure');
// Standard alias style.
const asserts = goog.require('goog.asserts');
// Namespace-based alias used to disambiguate.
const testingAsserts = goog.require('goog.testing.asserts');
// Standard destructuring into aliases.
const {clear, clone} = goog.require('goog.array');
const {Rgb} = goog.require('goog.color');
// Namespace-based destructuring into aliases in order to disambiguate.
const {SomeType: FooSomeType} = goog.requireType('foo.types');
const {clear: objectClear, clone: objectClone} = goog.require('goog.object');
// goog.require without an alias in order to trigger side effects.
/** @suppress {extraRequire} Initializes MyFramework. */
goog.require('my.framework.initialization');
不鼓励:
// If necessary to disambiguate, prefer PackageClass over SomeClass as it is
// closer to the format of the module name.
const SomeClass = goog.require('some.package.Class');
禁止:
// Extra terms must come from the namespace.
const MyClassForBizzing = goog.require('some.package.MyClass');
// Alias must include the entire final namespace component.
const MyClass = goog.require('some.package.MyClassForBizzing');
// Alias must not mask native type (should be `const JspbMap` here).
const Map = goog.require('jspb.Map');
// Don't break goog.require lines over 80 columns.
const SomeDataStructure =
goog.require('proto.identical.package.identifiers.SomeDataStructure');
// Alias must be based on the namespace.
const randomName = goog.require('something.else');
// Missing a space after the colon.
const {Foo:FooProto} = goog.require('some.package.proto.Foo');
// goog.requireType without an alias.
goog.requireType('some.package.with.a.Type');
/**
* @param {!some.unimported.Dependency} param All external types used in JSDoc
* annotations must be goog.require'd, unless declared in externs.
*/
function someFunction(param) {
// goog.require lines must be at the top level before any other code.
const alias = goog.require('my.long.name.alias');
// ...
}
3.7 文件实现
声明所有依赖项信息后(至少用一个空白行分隔),进行实际的实现。
这可以包括任何模块本地声明(常量,变量,类,函数等),以及任何导出的符号。