Google JavaScript 风格指南

请注意:本指南不再更新。 Google 建议迁移到 TypeScript,并遵循 TypeScript 指南

1 简介

本文档是对 Google JavaScript 编程语言源代码编码标准的完整定义。如果一个 JavaScript 源代码文件完全遵守本文档中的规则,则可以称其为符合 Google 风格

与其他编程风格指南类似,本文档涵盖的问题不仅包括格式的美观问题,还包括其他类型的约定或编码标准。但是,本文档主要侧重于我们普遍遵循的严格规则,避免提供无法明确执行(无论是通过人工还是工具)的建议。

1.1 术语说明

在本文档中,除非另有说明

  1. 术语注释始终指实现注释。我们不使用“文档注释”一词,而是使用通用术语“JSDoc”来表示 /** … */ 中的人类可读文本和机器可读的注释。

  2. 本风格指南在使用 必须 禁止 应该 不应该 可以 这些短语时,使用 RFC 2119 术语。术语 推荐 避免 分别对应于 应该 不应该 。命令式和声明式语句是规定性的,对应于 必须

其他“术语说明”会偶尔出现在文档中。

1.2 指南说明

本文档中的示例代码是非规范性的。也就是说,虽然示例符合 Google 风格,但它们可能没有说明表示代码的唯一时尚方式。示例中做出的可选格式选择不得作为规则强制执行。

2 源文件基础

2.1 文件名

文件名必须全部小写,并且可以包含下划线 (_) 或破折号 (-),但不能包含其他标点符号。遵循你的项目使用的约定。文件名扩展名必须是 .js

2.2 文件编码:UTF-8

源文件以 UTF-8 编码。

2.3 特殊字符

2.3.1 空格字符

除了行终止符序列之外,ASCII 水平空格字符 (0x20) 是唯一出现在源文件中的空格字符。这意味着

  1. 字符串文字中的所有其他空格字符都必须转义,并且

  2. 制表符用于缩进。

2.3.2 特殊转义序列

对于任何具有特殊转义序列的字符(\'\"\\\b\f\n\r\t\v),应使用该序列,而不是相应的数字转义符(例如 \x0a\u000a\u{a})。 永远不要使用旧式的八进制转义符。

2.3.3 非 ASCII 字符

对于其余的非 ASCII 字符,可以使用实际的 Unicode 字符(例如 )或等效的十六进制或 Unicode 转义符(例如 \u221e),具体取决于哪种方式使代码更易于阅读和理解

提示:在 Unicode 转义的情况下,甚至在使用实际 Unicode 字符时,解释性注释都可能非常有用。

/* Best: perfectly clear even without a comment. */
const units = 'μs';

/* Allowed: but unnecessary as μ is a printable character. */
const units = '\u03bcs'; // 'μs'

/* Good: use escapes for non-printable characters with a comment for clarity. */
return '\ufeff' + content;  // Prepend a byte order mark.
/* Poor: the reader has no idea what character this is. */
const units = '\u03bcs';

提示:切勿仅仅因为担心某些程序可能无法正确处理非 ASCII 字符而降低代码的可读性。如果发生这种情况,那些程序是错误的,必须修复它们。

3 源文件结构

所有新的源文件都应该是 goog.module 文件(包含 goog.module 调用的文件)或 ECMAScript (ES) 模块(使用 importexport 语句)。

文件由以下部分组成,按顺序

  1. 许可或版权信息(如果存在)
  2. @fileoverview JSDoc(如果存在)
  3. goog.module 语句(如果是 goog.module 文件)
  4. ES import 语句(如果是 ES 模块)
  5. goog.requiregoog.requireType 语句
  6. 文件的实现

一个空行分隔每个存在的部分,除了文件的实现,它可以由 1 或 2 个空行分隔。

如果许可或版权信息属于文件,则应在此处。

3.2 @fileoverview JSDoc(如果存在)

有关格式规则,请参阅 ??

3.3 goog.module 语句

所有 goog.module 文件都必须在单行上声明一个 goog.module 名称:包含 goog.module 声明的行不得换行,因此不受 80 列限制的约束。

goog.module 的整个参数定义了一个命名空间。它是包名称(反映代码所在的目录结构片段的标识符),加上可选的主要类/枚举/接口,以 lowerCamelCase 连接到末尾。

示例

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 导出

类、枚举、函数、常量和其他符号使用 exports 对象导出。 导出的符号可以直接在 exports 对象上定义,也可以在本地声明并单独导出。只有当符号打算在模块外部使用时才会被导出。未导出的模块本地符号不会声明为 @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';

不要将 exports 对象注释为 @const,因为它已经被编译器视为常量。

/** @const */
exports = {exportedFunction};

不要使用默认导出,因为它们不容易转换为 ES 模块语义。

exports = FancyClass;

3.4 ES 模块

ES 模块是使用 importexport 关键字的文件。

3.4.1 导入

import 语句不得换行,因此不受 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 * as name) 是从导入的文件名派生的 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';

某些库可能通常使用违反此命名方案的命名空间导入前缀,但普遍的开源使用使违反风格更具可读性。 目前属于此例外的唯一库是 threejs,它使用 THREE 前缀。

3.4.1.3.2 命名默认导入

默认导入名称从导入的文件名派生,并遵循 ?? 中的规则。

import MyClass from '../my-class.js';
import myFunction from '../my_function.js';
import SOME_CONSTANT from '../someconstant.js';

注意:一般来说,这不应该发生,因为默认导出被此风格指南禁止,请参阅 ??。 默认导入仅用于导入不符合此风格指南的模块。

3.4.1.3.3 命名命名导入

一般来说,通过命名导入 (import {name}) 导入的符号应保持相同的名称。 避免别名导入 (import {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 命名导出 vs 默认导出

在所有代码中使用命名导出。 你可以将 export 关键字应用于声明,或使用 export {name}; 语法。

不要使用默认导出。 导入模块必须为这些值命名,这可能导致模块之间的命名不一致。

// 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 导出的可变性

导出的变量不得在模块初始化之外发生变化。

如果需要改变,还有一些替代方法,包括导出对具有可变字段的对象的常量引用,或导出用于可变数据的访问器函数。

// 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.3 export from

export from 语句不得换行,因此不受 80 列限制的约束。 这适用于所有 export from 风格。

export {specificName} from './other.js';
export * from './another.js';

3.4.3 ES 模块中的循环依赖

即使 ECMAScript 规范允许这样做,也不要在 ES 模块之间创建循环。 请注意,可以使用 importexport 语句创建循环。

// 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 与 Closure 互操作

3.4.4.1 引用 goog

要引用 Closure goog 命名空间,请导入 Closure 的 goog.js

import * as goog from '../closure/goog/goog.js';

const {compute} = goog.require('a.name');

export const CONSTANT = compute();

goog.js 仅导出全局 goog 中的一部分属性,这些属性可以在 ES 模块中使用。

3.4.4.2 ES 模块中的 goog.require

ES 模块中的 goog.require 的工作方式与 goog.module 文件中的工作方式相同。 你可以 require 任何 Closure 命名空间符号(即,由 goog.providegoog.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 模块中声明 Closure 模块 ID

goog.declareModuleId 可用于 ES 模块中,以声明一个类似 goog.module 的模块 ID。这意味着该模块 ID 可以像 goog.module 一样被 goog.requiregoog.module.get 等引用,且该 goog.module 没有调用 goog.module.declareLegacyNamespace。它不会将模块 ID 创建为全局可用的 JavaScript 符号。

goog.declareModuleIdgoog.require (或 goog.module.get) 模块 ID 将始终返回模块对象(如同被 import * 导入一样)。因此,goog.declareModuleId 的参数应始终以 lowerCamelCaseName 结尾。

注意:在 ES 模块中调用 goog.module.declareLegacyNamespace 是错误的,它只能从 goog.module 文件中调用。 没有直接的方法将 legacy 命名空间与 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.module.declareLegacyNamespace() 语句之后,可以选择性地调用 goog.setTestOnly()

在 ES 模块中,import 语句之后,可以选择性地调用 goog.setTestOnly()

3.6 goog.requiregoog.requireType 语句

导入通过 goog.requiregoog.requireType 语句完成。 由 goog.require 语句导入的名称可以在代码和类型注释中使用,而由 goog.requireType 导入的名称只能在类型注释中使用。

goog.requiregoog.requireType 语句形成一个连续的块,没有空行。 此块位于 goog.module 声明之后, 用一个空行分隔goog.requiregoog.requireType 的整个参数是由单独文件中的 goog.module 定义的命名空间。 goog.requiregoog.requireType 语句不得出现在文件中的其他任何位置。

每个 goog.requiregoog.requireType 都被分配给一个常量别名,或者解构为几个常量别名。 这些别名是引用类型注释或代码中依赖项的唯一可接受方式。 完全限定的命名空间不得在任何地方使用,除非作为 goog.requiregoog.requireType 的参数。

例外:在 externs 文件中声明的类型、变量和函数必须在类型注释和代码中使用其完全限定名称。

goog.require 被分配给单个常量别名时,它必须与导入模块的命名空间的最后一个点分隔组件匹配。

例外:在某些情况下,可以使用命名空间的附加组件来形成更长的别名。 生成的别名必须保留原始标识符的大小写,以便它仍然可以正确识别其类型。 可以使用更长的别名来消除其他相同的别名的歧义,或者如果它可以显着提高可读性。 此外,必须使用更长的别名来防止屏蔽本机类型,如 ElementEventErrorMapPromise(有关更完整的列表,请参阅 标准内置对象Web API 在 MDN 上)。

重命名解构别名时,冒号后必须跟随一个空格,如 ?? 中所述。

一个文件不应包含同一命名空间的 goog.require 语句和 goog.requireType 语句。 如果导入的名称同时在代码和类型注释中使用,则应通过单个 goog.require 语句导入。

如果一个模块仅因其副作用而被导入,则该调用必须是 goog.require(而不是 goog.requireType),并且可以省略赋值。 需要注释来解释为什么需要这样做并抑制编译器警告。

这些行根据以下规则排序: 所有左侧带有名称的 require 语句首先出现,按这些名称的字母顺序排序。 然后是解构 require 语句,再次按左侧的名称排序。 最后,任何独立的 require 调用(通常这些调用是为了仅因其副作用而导入的模块)。

提示:无需记住此顺序并手动强制执行。 您可以依靠您的 IDE 来报告未正确排序的 require 语句。

如果长别名或模块名称导致一行超过 80 列的限制,则不得换行: require 行是 80 列限制的例外。

示例

// 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 {MyClass} = goog.require('some.package');
const {MyType} = goog.requireType('other.package');
const {clear, clone} = goog.require('goog.array');
const {Rgb} = goog.require('goog.color');
// Namespace-based destructuring into aliases used to disambiguate.
const {MyClass: NsMyClass} = goog.require('other.ns');
const {SomeType: FooSomeType} = goog.requireType('foo.types');
const {clear: objectClear, clone: objectClone} = goog.require('goog.object');
// Namespace-based destructuring into aliases used to prevent masking native type.
const {Element: RendererElement} = goog.require('web.renderer');
// Out of sequence namespace-based aliases used to improve readability.
// Also, require lines longer than 80 columns must not be wrapped.
const {SomeDataStructure: SomeDataStructureModel} = goog.requireType('identical.package.identifiers.models');
const {SomeDataStructure: SomeDataStructureProto} = goog.require('proto.identical.package.identifiers');
// goog.require without an alias in order to trigger side effects.
/** @suppress {extraRequire} Initializes MyFramework. */
goog.require('my.framework.initialization');

不推荐

// Some legacy code uses a "default export" style to export a single class, enum,
// record type, etc. Do not use this pattern in new JS.
// When using a "default export", prefer destructuring into aliases.
const MyClass = goog.require('some.package.MyClass');
const MyType = goog.requireType('some.package.MyType');
// 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 文件的实现

实际的实现位于声明所有依赖信息之后(至少用一个空行分隔)。

这可能包含任何模块本地声明(常量、变量、类、函数等)以及任何导出的符号。

4 格式化

术语说明类似块的构造是指类、函数、方法或大括号分隔的代码块的主体。 请注意,根据 ????,任何数组或对象字面量都可以选择被视为类似块的构造。

提示:使用 clang-format。 JavaScript 社区已投入精力以确保 clang-format 在 JavaScript 文件上 做正确的事情clang-format 与多个流行的编辑器集成。

4.1 大括号

4.1.1 大括号用于所有控制结构

所有控制结构(即 ifelsefordowhile 以及任何其他结构)都需要大括号,即使主体仅包含单个语句。 非空块的第一个语句必须从其自己的行开始。

不允许

if (someVeryLongCondition())
  doSomething();

for (let i = 0; i < foo.length; i++) bar(foo[i]);

例外:一个简单的 if 语句可以完全放在一行上而无需换行(并且没有 else 子句),当它可以提高可读性时,可以放在一行上而没有大括号。 这是控制结构可以省略大括号和换行的唯一情况。

if (shortCondition()) foo();

4.1.2 非空块:K&R 风格

大括号遵循 Kernighan 和 Ritchie 风格(埃及括号)用于非空块和类似块的构造

示例

class InnerClass {
  constructor() {}

  /** @param {number} foo */
  method(foo) {
    if (condition(foo)) {
      try {
        // Note: this might fail.
        something();
      } catch (err) {
        recover();
      }
    }
  }
}

4.1.3 空块:可以简洁

空块或类似块的构造可以在打开后立即关闭,中间没有字符、空格或换行符(即 {}),除非它是多块语句的一部分(直接包含多个块的语句:if/elsetry/catch/finally)。

示例

function doNothing() {}

不允许

if (condition) {
  // …
} else if (otherCondition) {} else {
  // …
}

try {
  // …
} catch (e) {}

4.2 块缩进:+2 个空格

每次打开新的块或类似块的构造时,缩进会增加两个空格。 当块结束时,缩进返回到之前的缩进级别。 缩进级别适用于整个块中的代码和注释。(参见 ?? 中的示例)。

4.2.1 数组字面量:可以选择 类似块

任何数组字面量都可以选择格式化为“类似块的构造”。 例如,以下都是有效的(不是详尽的列表)

const a = [
  0,
  1,
  2,
];

const b =
    [0, 1, 2];

const c = [0, 1, 2];

someMethod(foo, [
  0, 1, 2,
], bar);

允许其他组合,尤其是在强调元素之间的语义分组时,但不应仅用于减少较大数组的垂直大小。

4.2.2 对象字面量:可以选择 类似块

任何对象字面量都可以选择格式化为“类似块的构造”。 相同的示例适用于 ??。 例如,以下都是有效的(不是详尽的列表)

const a = {
  a: 0,
  b: 1,
};

const b =
    {a: 0, b: 1};
const c = {a: 0, b: 1};

someMethod(foo, {
  a: 0, b: 1,
}, bar);

4.2.3 类字面量

类字面量(无论是声明还是表达式)都缩进为块。 不要在方法之后或类声明的右大括号之后添加分号(包含类表达式的语句(例如赋值)仍然以分号结尾)。 对于继承,extends 关键字就足够了,除非超类是模板化的。 模板化类型的子类必须在 @extends JSDoc 注释中显式指定模板类型,即使它只是传递相同的模板名称。

示例

/** @template T */
class Foo {
  /** @param {T} x */
  constructor(x) {
    /** @type {T} */
    this.x = x;
  }
}

/** @extends {Foo<number>} */
class Bar extends Foo {
  constructor() {
    super(42);
  }
}

exports.Baz = class extends Bar {
  /** @return {number} */
  method() {
    return this.x;
  }
};
/** @extends {Bar} */ // <-- unnecessary @extends
exports.Baz = class extends Bar {
  /** @return {number} */
  method() {
    return this.x;
  }
};

4.2.4 函数表达式

在函数调用的参数列表中声明匿名函数时,函数的主体比前面的缩进深度多缩进两个空格。

示例

prefix.something.reallyLongFunctionName('whatever', (a1, a2) => {
  // Indent the function body +2 relative to indentation depth
  // of the 'prefix' statement one line above.
  if (a1.equals(a2)) {
    someOtherLongFunctionName(a1);
  } else {
    andNowForSomethingCompletelyDifferent(a2.parrot);
  }
});

some.reallyLongFunctionCall(arg1, arg2, arg3)
    .thatsWrapped()
    .then((result) => {
      // Indent the function body +2 relative to the indentation depth
      // of the '.then()' call.
      if (result) {
        result.use();
      }
    });

4.2.5 Switch 语句

与任何其他块一样,switch 块的内容缩进 +2。

在 switch 标签之后,会出现一个换行符,并且缩进级别会增加 +2,就像正在打开一个块一样。 如果词法作用域需要,可以使用显式块。 以下 switch 标签返回到之前的缩进级别,就像一个块已关闭一样。

break 和以下 case 之间可以有空行。

示例

switch (animal) {
  case Animal.BANDERSNATCH:
    handleBandersnatch();
    break;

  case Animal.JABBERWOCK:
    handleJabberwock();
    break;

  default:
    throw new Error('Unknown animal');
}

4.3 语句

4.3.1 每行一个语句

每个语句后跟一个换行符。

4.3.2 需要分号

每个语句都必须以分号结尾。 禁止依赖自动分号插入。

4.4 列限制:80

JavaScript 代码的列限制为 80 个字符。 除非如下所述,否则任何超过此限制的行都必须换行,如 ?? 中所述。

例外

  1. goog.modulegoog.requiregoog.requireType 语句(参见 ????)。
  2. ES 模块 importexport from 语句(参见 ????)。
  3. 在某些情况下,遵守列数限制是不可能的,或者会妨碍可发现性。例如:
    • 一个长的 URL,在源代码中应该可点击。
    • 一个旨在复制和粘贴的 shell 命令。
    • 一个可能需要完全复制或搜索的长字符串字面量(例如,一个长文件路径)。

4.5 换行

术语注意: 换行是将一段代码分成多行以遵守列数限制,而该段代码原本可以合法地放在一行中。

没有一个全面的、确定性的公式可以准确地展示在每种情况下如何换行。通常情况下,对同一段代码有几种有效的换行方式。

注意:虽然换行的典型原因是避免超出列数限制,但即使实际上可以放在列数限制内的代码,也可以由作者自行决定进行换行。

提示:提取方法或局部变量可能可以解决问题,而无需换行。

4.5.1 在哪里断开

换行的首要原则是:优先在更高的语法级别上断开。

推荐

currentEstimate =
    calc(currentEstimate + x * currentEstimate) /
        2.0;

不推荐

currentEstimate = calc(currentEstimate + x *
    currentEstimate) / 2.0;

在前面的例子中,从高到低的语法级别如下:赋值,除法,函数调用,参数,数字常量。

运算符的换行方式如下:

  1. 当一行在运算符处断开时,断点位于符号之后。(请注意,这与 Google Java 风格中使用的做法不同。)
    1. 这不适用于“点”(.),它实际上不是运算符。
  2. 方法或构造函数名称保持连接到它后面的左括号(()。
  3. 逗号(,)保持连接到它前面的 token。
  4. 永远不要在 return 和返回值之间添加换行符,因为这会改变代码的含义。
  5. 带有类型名称的 JSDoc 注释在“{”之后换行。这是必要的,因为带有可选类型(@const、@private、@param 等)的注释不会扫描下一行。

注意:换行的主要目标是使代码清晰,而不一定是使代码占据最少的行数。

4.5.2 延续行至少缩进 +4 个空格

换行时,第一行之后的每一行(每个延续行)都比原始行缩进至少 +4 个空格,除非它属于块缩进规则。

当有多个延续行时,可以根据需要将缩进更改为超过 +4。通常,在更深语法级别的延续行缩进量为 4 的更大倍数,并且只有当两行以语法上平行的元素开头时,它们才使用相同的缩进级别。

?? 讨论了不鼓励使用的、使用可变数量的空格来对齐某些 token 与前几行的做法。

4.6 空格

4.6.1 垂直空格

单个空行出现在

  1. 类或对象字面量中的连续方法之间
    1. 例外:对象字面量中两个连续属性定义(它们之间没有其他代码)之间的空行是可选的。这些空行根据需要用于创建字段的逻辑分组
  2. 在方法主体中,谨慎地用于创建语句的逻辑分组。不允许在函数主体的开头或结尾处使用空行。
  3. 可选地在类或对象字面量中的第一个方法之前或最后一个方法之后(既不鼓励也不阻止)。
  4. 根据本文档的其他部分的要求(例如 ??)。

允许使用多个连续空行,但从不需要(也不鼓励)。

4.6.2 水平空格

水平空格的使用取决于位置,并分为三大类:前导(在一行的开头)、尾随(在一行的结尾)和内部。 前导空格(即,缩进)在其他地方讨论。 禁止尾随空格。

除了语言或其他样式规则要求的以外,并且除了字面量、注释和 JSDoc 之外,单个内部 ASCII 空格也仅出现在以下位置

  1. 将任何保留字(例如 ifforcatch),除了 functionsuper,与该行上紧随其后的左括号(()分开。
  2. 将任何保留字(例如 elsecatch)与该行上它前面的右花括号(})分开。
  3. 在任何左花括号({)之前,有两个例外:
    1. 作为函数或数组字面量中第一个元素的对象字面量之前 (例如 foo({a: [{c: d}]}))。
    2. 在模板展开中,因为它被语言禁止 (例如,有效: `ab${1 + 2}cd`,无效: `xy$ {3}z`)。
  4. 在任何二元或三元运算符的两侧。
  5. 在逗号(,)或分号(;)之后。请注意,永远不允许在这些字符之前放置空格。
  6. 在对象字面量中,在冒号(:)之后。
  7. 在双斜杠(//)的两侧,双斜杠是行尾注释的开头。 在这里,允许多个空格,但不是必需的。
  8. 在块注释字符之后和结束字符的两侧(例如,对于简短形式的类型声明、强制转换和参数名称注释:this.foo = /** @type {number} */ (bar); 或 function(/** string */ foo) {; 或 baz(/* buzz= */ true))。

4.6.3 水平对齐:不鼓励

术语注意水平对齐是一种在代码中添加可变数量的额外空格的做法,目的是使某些 token 看起来直接位于前几行的某些其他 token 下方。

允许使用此做法,但 Google 样式通常不鼓励使用。 甚至不需要维护已经使用水平对齐的地方的水平对齐。

这是一个没有对齐的示例,后跟一个对齐的示例。 两者都是允许的,但不鼓励后者

{
  tiny: 42, // this is great
  longer: 435, // this too
};

{
  tiny:   42,  // permitted, but future edits
  longer: 435, // may leave it unaligned
};

提示:对齐可以帮助提高可读性,但会给将来的维护带来问题。 考虑一下将来需要只接触一行代码的更改。 此更改可能会使以前令人满意的格式变得混乱,这是允许的。 更常见的是,它会提示编码员(可能就是你)也调整附近行的空格,这可能会引发一系列级联的重新格式化。 那一个行更改现在具有“爆炸半径”。 最坏的情况是,这会导致毫无意义的忙碌,但最好的情况是,它仍然会破坏版本历史信息,减慢审阅者的速度并加剧合并冲突。

4.6.4 函数参数

建议将所有函数参数放在与函数名称相同的行上。 如果这样做会超出 80 列的限制,则必须以可读的方式对参数进行换行。 为了节省空间,你可以尽可能接近 80 列的位置进行换行,或者将每个参数放在自己的行上以增强可读性。 缩进应为四个空格。 允许与括号对齐,但不鼓励。 以下是最常见的参数换行模式

// Arguments start on a new line, indented four spaces. Preferred when the
// arguments don't fit on the same line with the function name (or the keyword
// "function") but fit entirely on the second line. Works with very long
// function names, survives renaming without reindenting, low on space.
doSomething(
    descriptiveArgumentOne, descriptiveArgumentTwo, descriptiveArgumentThree) {
  // …
}

// If the argument list is longer, wrap at 80. Uses less vertical space,
// but violates the rectangle rule and is thus not recommended.
doSomething(veryDescriptiveArgumentNumberOne, veryDescriptiveArgumentTwo,
    tableModelEventHandlerProxy, artichokeDescriptorAdapterIterator) {
  // …
}

// Four-space, one argument per line.  Works with long function names,
// survives renaming, and emphasizes each argument.
doSomething(
    veryDescriptiveArgumentNumberOne,
    veryDescriptiveArgumentTwo,
    tableModelEventHandlerProxy,
    artichokeDescriptorAdapterIterator) {
  // …
}

4.7 分组括号:推荐

只有当作者和审阅者都同意没有合理的可能性会由于没有括号而误解代码,或者括号会使代码更易于阅读时,才可以省略可选的分组括号。 假设每个读者都记住了整个运算符优先级表是合理的。

不要在 deletetypeofvoidreturnthrowcaseinofyield 之后的整个表达式周围使用不必要的括号。

类型转换需要使用括号:/** @type {!Foo} */ (foo)

4.8 注释

本节讨论的是实现注释。 JSDoc 在 ?? 中单独讨论。

4.8.1 块注释风格

块注释的缩进级别与周围的代码相同。它们可以是 /* … */// 风格。对于多行 /* … */ 注释,后续行必须以 * 开头,并与上一行的 * 对齐,以使注释显而易见,而无需额外的上下文。

/*
 * This is
 * okay.
 */

// And so
// is this.

/* This is fine, too. */

注释没有包含在用星号或其他字符绘制的框中。

不要将 JSDoc (/** … */) 用于实现注释。

4.8.2 参数名称注释

如果值和方法名称不能充分表达含义,并且重构方法以使其更清晰是不可行的,则应使用“参数名称”注释。 它们的首选格式是在值之前加上“=”。

someFunction(obviousParam, /* shouldRender= */ true, /* name= */ 'hello');

为了与周围的代码保持一致,你可以将它们放在值之后,不带“=”。

someFunction(obviousParam, true /* shouldRender */, 'hello' /* name */);

5 语言特性

JavaScript 包含许多可疑的(甚至危险的)特性。 本节描述了哪些特性可以使用或不能使用,以及对其使用的任何其他约束。

本样式指南中未讨论的语言特性可以在没有使用建议的情况下使用。

5.1 局部变量声明

5.1.1 使用 constlet

使用 constlet 声明所有局部变量。 默认情况下使用 const,除非变量需要重新赋值。 不得使用 var 关键字。

5.1.2 每个声明一个变量

每个局部变量声明只声明一个变量:不使用诸如 let a = 1, b = 2; 之类的声明。

5.1.3 需要时声明,尽快初始化

局部变量不是习惯性地在其包含块或类似块的结构的开头声明。 相反,局部变量声明的位置应尽可能接近它们首次使用的地方(在合理的范围内),以最大程度地减少其作用域,并尽快初始化。

5.1.4 根据需要声明类型

JSDoc 类型注释可以添加到声明的上一行,或者,如果没有其他 JSDoc 存在,则可以内联在变量名称之前。

示例

const /** !Array<number> */ data = [];

/**
 * Some description.
 * @type {!Array<number>}
 */
const data = [];

不允许混合使用内联和 JSDoc 样式:编译器只会处理第一个 JsDoc,内联注释将丢失。

/** Some description. */
const /** !Array<number> */ data = [];

提示:在许多情况下,编译器可以推断出模板化类型,但不能推断出其参数。 当初始化字面量或构造函数调用不包含模板参数类型的任何值时(例如,空数组、对象、MapSet),或者变量在闭包中被修改时,尤其如此。 在这些情况下,局部变量类型注释特别有用,因为否则编译器会将模板参数推断为未知。

5.2 数组字面量

5.2.1 使用尾随逗号

每当最后一个元素和右括号之间存在换行符时,都应包含尾随逗号。

示例

const values = [
  'first value',
  'second value',
];

5.2.2 不要使用可变参数的 Array 构造函数

如果添加或删除参数,构造函数容易出错。请改用字面量。

不允许

const a1 = new Array(x1, x2, x3);
const a2 = new Array(x1, x2);
const a3 = new Array(x1);
const a4 = new Array();

除了第三种情况,这会如预期工作:如果 x1 是一个整数,那么 a3 是一个大小为 x1 的数组,其中所有元素都是 undefined。如果 x1 是任何其他数字,则会抛出异常,如果它是任何其他类型,那么它将是一个单元素数组。

请改为编写:

const a1 = [x1, x2, x3];
const a2 = [x1, x2];
const a3 = [x1];
const a4 = [];

在适当的时候,允许使用 new Array(length) 显式分配给定长度的数组。

5.2.3 非数字属性

不要在数组上定义或使用非数字属性(length 除外)。请改用 Map(或 Object)。

5.2.4 解构

数组字面量可以用于赋值的左侧以执行解构(例如,当从单个数组或可迭代对象中解包多个值时)。可以包含一个最终的 rest 元素(... 和变量名之间没有空格)。如果元素未使用,则应省略。

const [a, b, c, ...rest] = generateResults();
let [, b,, d] = someArray;

解构也可以用于函数参数(请注意,需要一个参数名称,但会被忽略)。如果解构的数组参数是可选的,则始终将 [] 指定为默认值,并在左侧提供默认值。

/** @param {!Array<number>=} param1 */
function optionalDestructuring([a = 4, b = 2] = []) { … };

不允许

function badDestructuring([a, b] = [4, 2]) { … };

提示:对于将多个值(取消)打包到函数的参数或返回值中,尽可能优先使用对象解构而不是数组解构,因为它允许命名各个元素并为每个元素指定不同的类型。

5.2.5 展开运算符

数组字面量可以包含展开运算符 (...) 以将元素从一个或多个其他可迭代对象中展开。应使用展开运算符,而不是使用更麻烦的 Array.prototype 构造。在 ... 之后没有空格。

示例

[...foo]   // preferred over Array.prototype.slice.call(foo)
[...foo, ...bar]   // preferred over foo.concat(bar)

5.3 对象字面量

5.3.1 使用尾随逗号

只要最后一个属性和右大括号之间存在换行,就包含尾随逗号。

5.3.2 不要使用 Object 构造函数

虽然 Object 没有与 Array 相同的问题,但为了保持一致性,仍然不允许使用它。请改用对象字面量({}{a: 0, b: 1, c: 2})。

5.3.3 不要混合使用带引号和不带引号的键

对象字面量可以表示结构体(带有不带引号的键和/或符号)或字典(带有带引号的键和/或计算出的键)。不要在单个对象字面量中混合使用这些键类型。

不允许

{
  width: 42, // struct-style unquoted key
  'maxWidth': 43, // dict-style quoted key
}

这也适用于将属性名称传递给函数,例如 hasOwnProperty。特别是,这样做会在编译后的代码中中断,因为编译器无法重命名/混淆字符串字面量。

不允许

/** @type {{width: number, maxWidth: (number|undefined)}} */
const o = {width: 42};
if (o.hasOwnProperty('maxWidth')) {
  ...
}

最好这样实现:

/** @type {{width: number, maxWidth: (number|undefined)}} */
const o = {width: 42};
if (o.maxWidth != null) {
  ...
}

5.3.4 计算属性名称

允许使用计算属性名称(例如,{['key' + foo()]: 42}),并且它们被认为是字典样式的(带引号的)键(即,不得与不带引号的键混合使用),除非计算属性是一个 symbol(例如,[Symbol.iterator])。枚举值也可以用于计算键,但不应在同一个字面量中与非枚举键混合使用。

5.3.5 方法简写

可以使用方法简写 ({method() {… }}) 在对象字面量上定义方法,以代替紧跟在 function 或箭头函数字面量之后的冒号。

示例

return {
  stuff: 'candy',
  method() {
    return this.stuff;  // Returns 'candy'
  },
};

请注意,方法简写或 function 中的 this 指的是对象字面量本身,而箭头函数中的 this 指的是对象字面量之外的作用域。

示例

class {
  getObjectLiteral() {
    this.stuff = 'fruit';
    return {
      stuff: 'candy',
      method: () => this.stuff,  // Returns 'fruit'
    };
  }
}

5.3.6 简写属性

允许在对象字面量上使用简写属性。

示例

const foo = 1;
const bar = 2;
const obj = {
  foo,
  bar,
  method() { return this.foo + this.bar; },
};
assertEquals(3, obj.method());

5.3.7 解构

对象解构模式可以用于赋值的左侧,以执行解构并从单个对象中解包多个值。

解构的对象也可以用作函数参数,但应尽可能保持简单:单层不带引号的简写属性。在参数解构中不得使用更深层次的嵌套和计算属性。在解构参数的左侧指定任何默认值({str = 'some default'} = {},而不是 {str} = {str: 'some default'}),如果解构的对象本身是可选的,则它必须默认为 {}。解构参数的 JSDoc 可以被赋予任何名称(该名称未使用,但编译器需要它)。

示例

/**
 * @param {string} ordinary
 * @param {{num: (number|undefined), str: (string|undefined)}=} param1
 *     num: The number of times to do something.
 *     str: A string to do stuff to.
 */
function destructured(ordinary, {num, str = 'some default'} = {}) {}

不允许

/** @param {{x: {num: (number|undefined), str: (string|undefined)}}} param1 */
function nestedTooDeeply({x: {num, str}}) {};
/** @param {{num: (number|undefined), str: (string|undefined)}=} param1 */
function nonShorthandProperty({num: a, str: b} = {}) {};
/** @param {{a: number, b: number}} param1 */
function computedKey({a, b, [a + b]: c}) {};
/** @param {{a: number, b: string}=} param1 */
function nontrivialDefault({a, b} = {a: 2, b: 4}) {};

解构也可以用于 goog.require 语句,在这种情况下不得包装:整个语句占据一行,无论它有多长(参见 ??)。

5.3.8 枚举

枚举通过将 @enum 注释添加到对象字面量来定义。枚举必须是模块本地的或直接分配给 exports,而不是嵌套在类型或对象下。

定义枚举后,不能向枚举添加其他属性。枚举必须是常量。所有枚举值必须是字符串字面量或数字。

/**
 * Supported temperature scales.
 * @enum {string}
 */
const TemperatureScale = {
  CELSIUS: 'celsius',
  FAHRENHEIT: 'fahrenheit',
};

/**
 * An enum with two values.
 * @enum {number}
 */
const Value = {
  /** The value used shall have been the first. */
  FIRST_VALUE: 1,
  /** The second among two values. */
  SECOND_VALUE: 2,
};

对于字符串枚举,所有值必须是静态初始化的,而不是使用算术运算符、模板字面量替换、函数调用甚至变量引用来计算的。

const ABSOLUTE_ZERO = '-273°F';

/**
 * Not supported computed values in string enum.
 * @enum {string}
 */
const TemperatureInFahrenheit = {
  MIN_POSSIBLE: ABSOLUTE_ZERO,
  ZERO_FAHRENHEIT: 0 + '°F',
  ONE_FAHRENHEIT: `${Values.FIRST_VALUE}°F`,
  TWO_FAHRENHEIT: Values.SECOND_VALUE + '°F',
  SOME_FAHRENHEIT: getTemperatureInFahrenheit() + '°F',
};

注意:虽然 TypeScript 支持一些用于枚举值的更多模式(例如 A: 'a'+'b' 等),但仅允许字符串字面量和数字作为枚举值的限制是为了帮助迁移到 TypeScript。对于复杂的值,请考虑使用不带 @enum 的 const 对象。

5.4 类

5.4.1 构造函数

构造函数是可选的。子类构造函数必须在设置任何字段或以其他方式访问 this 之前调用 super()。接口应在构造函数中声明非方法属性。

5.4.2 字段

在构造函数中定义具体对象的所有字段(即方法之外的所有属性)。用 @const 注释从不重新分配的字段(这些字段不必是深度不可变的)。用适当的可见性注释 (@private@protected@package) 注释非公共字段。@private 字段的名称可以选择以一个下划线结尾。字段不得在构造函数中的嵌套作用域内或在具体类的 prototype 上定义。

示例

class Foo {
  constructor() {
    /** @private @const {!Bar} */
    this.bar_ = computeBar();

    /** @protected @const {!Baz} */
    this.baz = computeBaz();
  }
}

提示:在构造函数完成后,不应向实例添加或从中删除属性,因为它会严重阻碍 VM 的优化能力。如有必要,稍后初始化的字段应在构造函数中显式设置为 undefined,以防止稍后的形状更改。将 @struct 添加到对象将检查是否未添加/访问未声明的属性。默认情况下,类会添加此选项。

5.4.3 计算属性

仅当属性是符号时,才可以在类中使用计算属性。不允许使用字典样式的属性(即,带引号或计算的非符号键,如 ?? 中定义的那样)。应为任何逻辑上可迭代的类定义一个 [Symbol.iterator] 方法。除此之外,应谨慎使用 Symbol

提示:注意使用任何其他内置符号(例如,Symbol.isConcatSpreadable),因为它们不会被编译器填充,因此在旧版本的浏览器中不起作用。

5.4.4 静态方法

在不影响可读性的情况下,优先使用模块本地函数而不是私有静态方法。

代码不应依赖于静态方法的动态调度,因为它会干扰 Closure Compiler 优化。静态方法应仅在基类本身上调用。静态方法不应在包含可能是构造函数或子类构造函数的动态实例的变量上调用(如果这样做,则必须使用 @nocollapse 定义),并且不得在未定义方法本身的子类上直接调用。不要在静态方法中访问 this

不允许

// Context for the examples below; by itself this code is allowed.
class Base {
  /** @nocollapse */ static foo() {}
}
class Sub extends Base {}

// discouraged: don't call static methods dynamically
function callFoo(cls) { cls.foo(); }

// Disallowed: don't call static methods on subclasses that don't define it themselves
Sub.foo();

// Disallowed: don't access this in static methods.
class Clazz {
  static foo() {
    return this.staticField;
  }
}
Class.staticField = 1;

5.4.5 旧式类声明

虽然首选 ES6 类,但在某些情况下,ES6 类可能不可行。例如:

  1. 如果存在或将存在子类(包括创建子类的框架),这些子类不能立即更改为使用 ES6 类语法。如果这样的类要使用 ES6 语法,则所有不使用 ES6 类语法的下游子类都需要修改。

  2. 需要在使用超类构造函数之前知道 this 值的框架,因为带有 ES6 超类的构造函数在调用 super 返回之前无权访问实例 this 值。

在所有其他方面,样式指南仍然适用于此代码:应在适当的时候使用 letconst、默认参数、rest 和箭头函数。

goog.defineClass 允许类似于 ES6 类语法的类式定义。

let C = goog.defineClass(S, {
  /**
   * @param {string} value
   */
  constructor(value) {
    S.call(this, 2);
    /** @const */
    this.prop = value;
  },

  /**
   * @param {string} param
   * @return {number}
   */
  method(param) {
    return 0;
  },
});

或者,虽然应为所有新代码优先使用 goog.defineClass,但也允许使用更传统的语法。

/**
  * @constructor @extends {S}
  * @param {string} value
  */
function C(value) {
  S.call(this, 2);
  /** @const */
  this.prop = value;
}
goog.inherits(C, S);

/**
 * @param {string} param
 * @return {number}
 */
C.prototype.method = function(param) {
  return 0;
};

如果存在超类,则应在调用超类构造函数后在构造函数中定义每个实例的属性。方法应在构造函数的原型上定义。

正确定义构造函数原型层次结构比最初看起来更难!因此,最好使用来自 Closure Library goog.inherits

5.4.6 不要直接操作 prototype

与定义 prototype 属性相比,class 关键字允许更清晰和更可读的类定义。普通的实现代码不应该操纵这些对象,尽管它们对于定义 ?? 中定义的类仍然有用。明确禁止混合和修改内置对象的原型。

例外情况:框架代码(例如 Polymer 或 Angular)可能需要使用 prototype,并且不应采用更糟糕的解决方法来避免这样做。

5.4.7 Getters 和 Setters

不要使用 JavaScript getter 和 setter 属性。它们可能令人惊讶且难以推理,并且在编译器中的支持有限。请改为提供普通方法。

例外情况:在某些情况下,定义 getter 或 setter 是不可避免的(例如,Angular 和 Polymer 等数据绑定框架,或为了与无法调整的外部 API 兼容)。仅在这些情况下,才谨慎使用 getter 和 setter,前提是它们使用 getset 简写方法关键字或 Object.defineProperties 定义(而不是 Object.defineProperty,这会干扰属性重命名)。Getters 不能更改可观察状态。

不允许

class Foo {
  get next() { return this.nextId++; }
}

5.4.8 重写 toString

可以重写 toString 方法,但必须始终成功且永远没有可见的副作用。

提示:特别要注意从 toString 调用其他方法,因为异常情况可能会导致无限循环。

5.4.9 接口

可以使用 @interface@record 声明接口。使用 @record 声明的接口可以由类或对象字面量显式(即通过 @implements)或隐式地实现。

接口上的所有方法都必须是非静态的,并且方法体必须是空块。字段必须在类构造函数中声明为未初始化的成员。

示例

/**
 * Something that can frobnicate.
 * @record
 */
class Frobnicator {
  constructor() {
    /** @type {number} The number of attempts before giving up. */
    this.attempts;
  }

  /**
   * Performs the frobnication according to the given strategy.
   * @param {!FrobnicationStrategy} strategy
   */
  frobnicate(strategy) {}
}

5.4.10 抽象类

在适当的时候使用抽象类。抽象类和抽象方法必须使用 @abstract 注解。不要使用 goog.abstractMethod。参见 抽象类和抽象方法

5.4.11 不要创建静态容器类

不要为了命名空间而使用仅包含静态方法或属性的容器类。

// container.js
// Bad: Container is an exported class that has only static methods and fields.
class Container {
  /** @return {number} */
  static bar() {
    return 1;
  }
}

/** @const {number} */
Container.FOO = 1;

exports = {Container};

相反,导出单个常量和函数

/** @return {number} */
exports.bar = () => {
  return 1;
}

/** @const {number} */
exports.FOO = 1;

5.4.12 不要定义嵌套的命名空间

不要在另一个模块局部名称上定义嵌套类型(例如,类、typedef、enum、interface)。

// foo.js
goog.module('my.namespace');

class Foo {...}

Foo.Bar = class {...};

/** @enum {number} */
Foo.Baz = {...};

/** @typedef {{value: number}} */
Foo.Qux;

/** @interface */
Foo.Quuz = class {...}

exports.Foo = Foo;

这些值应该是顶层导出。为这些值选择清晰的名称(例如,对于可以嵌套在 Foo 上的转换器,使用 FooConverter)。但是,当模块名称与类名的一部分冗余时,请考虑省略冗余:foo.Foofoo.Converter 而不是 foo.Foofoo.FooConverter。导入者可以在必要时添加前缀以提高清晰度(例如,import {Converter as FooConverter} from './foo';),但当将整个模块作为命名空间导入时,无法轻易删除冗余。

// foo.js
goog.module('my.namespace');

class Foo {...}

class FooBar {...}

/** @enum {string} */
let FooBaz = {...};

/** @typedef {{value: number}} */
let FooQux;

/** @interface */
class FooQuuz {...};

export = {
  Foo,
  FooBar,
  FooBaz,
  FooQux,
  FooQuuz,
};

5.5 函数

5.5.1 顶层函数

顶层函数可以直接在 exports 对象上定义,也可以在本地声明并选择性地导出。有关导出的更多信息,请参阅 ??

示例

/** @param {string} str */
exports.processString = (str) => {
  // Process the string.
};
/** @param {string} str */
const processString = (str) => {
  // Process the string.
};

exports = {processString};

5.5.2 嵌套函数和闭包

函数可以包含嵌套的函数定义。如果给函数一个名称很有用,则应将其分配给本地 const

5.5.3 箭头函数

箭头函数提供简洁的函数语法,并简化了嵌套函数的作用域 this。对于嵌套函数,优先使用箭头函数而不是 function 关键字(但请参见 ??)。

优先使用箭头函数而不是其他 this 作用域方法,例如 f.bind(this)goog.bind(f, this)const self = this。箭头函数对于调用回调特别有用,因为它们允许显式指定要传递给回调的参数,而绑定会盲目地传递所有参数。

箭头的左侧包含零个或多个参数。如果只有一个非解构参数,则参数周围的括号是可选的。使用括号时,可以指定内联参数类型(请参见 ??)。

提示:即使对于单参数箭头函数也始终使用括号,可以避免添加参数但忘记添加括号,从而导致代码可解析但不再按预期工作的情况。

箭头的右侧包含函数的主体。默认情况下,主体是一个块语句(零个或多个语句,用大括号括起来)。如果满足以下任一条件,主体也可以是隐式返回的单个表达式:程序逻辑需要返回值,或者 void 运算符位于单个函数或方法调用之前(使用 void 确保返回 undefined,防止泄漏值,并传达意图)。如果它提高了可读性(例如,对于短或简单的表达式),则首选单表达式形式。

示例

/**
 * Arrow functions can be documented just like normal functions.
 * @param {number} numParam A number to add.
 * @param {string} strParam Another number to add that happens to be a string.
 * @return {number} The sum of the two parameters.
 */
const moduleLocalFunc = (numParam, strParam) => numParam + Number(strParam);

// Uses the single expression syntax with `void` because the program logic does
// not require returning a value.
getValue((result) => void alert(`Got ${result}`));

class CallbackExample {
  constructor() {
    /** @private {number} */
    this.cachedValue_ = 0;

    // For inline callbacks, you can use inline typing for parameters.
    // Uses a block statement because the value of the single expression should
    // not be returned and the expression is not a single function call.
    getNullableValue((/** ?number */ result) => {
      this.cachedValue_ = result == null ? 0 : result;
    });
  }
}

不允许

/**
 * A function with no params and no returned value.
 * This single expression body usage is illegal because the program logic does
 * not require returning a value and we're missing the `void` operator.
 */
const moduleLocalFunc = () => anotherFunction();

5.5.4 生成器

生成器启用许多有用的抽象,可以根据需要使用。

定义生成器函数时,如果存在 function 关键字,则将 * 附加到该关键字,并用空格将其与函数名称分隔开。使用委托 yield 时,将 * 附加到 yield 关键字。

示例

/** @return {!Iterator<number>} */
function* gen1() {
  yield 42;
}

/** @return {!Iterator<number>} */
const gen2 = function*() {
  yield* gen1();
}

class SomeClass {
  /** @return {!Iterator<number>} */
  * gen() {
    yield 42;
  }
}

5.5.5 参数和返回类型

函数参数和返回类型通常应使用 JSDoc 注释进行文档化。有关更多信息,请参见 ??

5.5.5.1 默认参数

允许使用参数列表中的等号运算符来指定可选参数。可选参数必须在等号运算符的两侧包含空格,必须与必需参数的命名方式完全相同(即,不以 opt_ 为前缀),在其 JSDoc 类型中使用 = 后缀,位于必需参数之后,并且不使用产生可观察副作用的初始化器。与具体函数相反,抽象方法和接口方法必须省略默认参数值。

示例

/**
 * @param {string} required This parameter is always needed.
 * @param {string=} optional This parameter can be omitted.
 * @param {!Node=} node Another optional parameter.
 */
function maybeDoSomething(required, optional = '', node = undefined) {}

/** @interface */
class MyInterface {
  /**
   * Interface and abstract methods must omit default parameter values.
   * @param {string=} optional
   */
  someMethod(optional) {}
}

谨慎使用默认参数。当存在少量没有自然顺序的可选参数时,首选解构(如 ?? 中所示)来创建可读的 API。

注意:与 Python 的默认参数不同,可以使用返回新的可变对象的初始化器(例如 {}[]),因为每次使用默认值时都会评估该初始化器,因此单个对象不会在调用之间共享。

提示:虽然可以将包括函数调用在内的任意表达式用作初始化器,但应尽可能保持简单。避免暴露共享的可变状态的初始化器,因为这很容易在函数调用之间引入意外的耦合。

5.5.5.2 Rest 参数

使用 rest 参数而不是访问 arguments。Rest 参数在其 JSDoc 中使用 ... 前缀进行类型化。Rest 参数必须是列表中的最后一个参数。... 和参数名称之间没有空格。不要将 rest 参数命名为 var_args。永远不要将局部变量或参数命名为 arguments,这会令人困惑地覆盖内置名称。

示例

/**
 * @param {!Array<string>} array This is an ordinary parameter.
 * @param {...number} numbers The remainder of arguments are all numbers.
 */
function variadic(array, ...numbers) {}

5.5.6 泛型

必要时,使用函数或方法定义上方的 JSDoc 中的 @template TYPE 声明泛型函数和方法。

5.5.7 Spread 运算符

函数调用可以使用 spread 运算符 (...)。当将数组或可迭代对象解压缩为可变函数的多个参数时,首选 spread 运算符而不是 Function.prototype.apply... 之后没有空格。

示例

function myFunction(...elements) {}
myFunction(...array, ...iterable, ...generator());

5.6 字符串字面量

5.6.1 使用单引号

普通字符串字面量使用单引号 (') 分隔,而不是双引号 (")。

提示:如果字符串包含单引号字符,请考虑使用模板字符串以避免转义引号。

普通字符串字面量不能跨越多行。

5.6.2 模板字面量

对于复杂的字符串连接,特别是涉及多个字符串字面量时,请使用模板字面量(用 ` 分隔)。模板字面量可以跨越多行。

如果模板字面量跨越多行,则不需要遵循封闭块的缩进,但如果添加的空格无关紧要,则可以这样做。

示例

function arithmetic(a, b) {
  return `Here is a table of arithmetic operations:
${a} + ${b} = ${a + b}
${a} - ${b} = ${a - b}
${a} * ${b} = ${a * b}
${a} / ${b} = ${a / b}`;
}

5.6.3 没有行继续符

不要在普通或模板字符串字面量中使用行继续符(即,使用反斜杠结束字符串字面量中的一行)。即使 ES5 允许这样做,但如果在斜杠后出现任何尾随空格,也可能导致棘手的错误,并且对于读者来说不太明显。

不允许

const longString = 'This is a very long string that far exceeds the 80 \
    column limit. It unfortunately contains long stretches of spaces due \
    to how the continued lines are indented.';

请改为编写:

const longString = 'This is a very long string that far exceeds the 80 ' +
    'column limit. It does not contain long stretches of spaces since ' +
    'the concatenated strings are cleaner.';

5.7 数字字面量

数字可以用十进制、十六进制、八进制或二进制指定。对于十六进制、八进制和二进制,分别使用小写字母的 0x0o0b 前缀。除非紧接其后的是 xob,否则永远不要包含前导零。

5.8 控制结构

5.8.1 For 循环

使用 ES6,该语言现在有三种不同类型的 for 循环。所有都可以使用,但应尽可能首选 for-of 循环。

for-in 循环只能在 dict 风格的对象上使用(参见 ??),并且不应用于迭代数组。Object.prototype.hasOwnProperty 应在 for-in 循环中使用,以排除不需要的原型属性。尽可能首选 for-ofObject.keys 而不是 for-in

5.8.2 异常

异常是该语言的重要组成部分,应在发生异常情况时使用。始终抛出 ErrorError 的子类:永远不要抛出字符串字面量或其他对象。构造 Error 时始终使用 new

这种处理方式也适用于 Promise 拒绝值,因为 Promise.reject(obj) 等同于异步函数中的 throw obj;

自定义异常提供了一种很好的方式来传递函数中的其他错误信息。当原生 Error 类型不足时,应定义和使用它们。

首选抛出异常而不是临时错误处理方法(例如传递错误容器引用类型,或返回带有错误属性的对象)。

5.8.2.1 空的 catch 块

在响应捕获的异常时,什么都不做很少是正确的做法。当确实适合在 catch 块中不采取任何措施时,将在注释中解释证明其合理的原因。

try {
  return handleNumericResponse(response);
} catch (ok) {
  // it's not numeric; that's fine, just continue
}
return handleTextResponse(response);

不允许

  try {
    shouldFail();
    fail('expected an error');
  } catch (expected) {
  }

提示:与其他一些语言不同,上面的模式根本不起作用,因为这将捕获 fail 抛出的错误。请改用 assertThrows()

5.8.3 Switch 语句

术语说明:在 switch 块的大括号内是一个或多个语句组。每个语句组由一个或多个 switch 标签(case FOO:default:)组成,后跟一个或多个语句。

5.8.3.1 Fall-through:注释

在 switch 块中,每个语句组要么突然终止(带有 breakreturnthrow 异常),要么用注释标记以指示执行将或可能继续到下一个语句组。任何传达 fall-through 想法的注释都足够了(通常是 // fall through)。此特殊注释在 switch 块的最后一个语句组中不是必需的。

示例

switch (input) {
  case 1:
  case 2:
    prepareOneOrTwo();
  // fall through
  case 3:
    handleOneTwoOrThree();
    break;
  default:
    handleLargeNumber(input);
}
5.8.3.2 存在 default case

每个 switch 语句都包含一个 default 语句组,即使它不包含任何代码。default 语句组必须是最后一个。

5.9 this

仅在类构造函数和方法中,在类构造函数和方法中定义的箭头函数中,或在立即封闭函数的 JSDoc 中声明了显式 @this 的函数中使用 this

永远不要使用 this 来引用全局对象、eval 的上下文、事件的目标或不必要地 call()apply() 过的函数。

5.10 相等性检查

除非在下面记录的情况下,否则使用恒等运算符 (===/!==)。

5.10.1 强制类型转换是期望的情况

同时捕获 nullundefined

if (someObjectOrPrimitive == null) {
  // Checking for null catches both null and undefined for objects and
  // primitives, but does not catch other falsy values like 0 or the empty
  // string.
}

5.11 禁止的功能

5.11.1 with

不要使用 with 关键字。它使您的代码更难理解,并且自 ES5 以来已在严格模式中被禁止。

5.11.2 动态代码评估

不要使用 evalFunction(...string) 构造函数(代码加载器除外)。这些功能可能很危险,并且在 CSP 环境中根本不起作用。

5.11.3 自动分号插入

始终使用分号终止语句(如上所述,函数和类声明除外)。

5.11.4 非标准特性

不要使用非标准特性。这包括已被移除的旧特性(例如,WeakMap.clear),尚未标准化的新特性(例如,当前的 TC39 工作草案、任何阶段的提案或已提出但尚未完成的 Web 标准),或仅在某些浏览器中实现的专有特性。仅使用当前 ECMA-262 或 WHATWG 标准中定义的特性。(请注意,针对特定 API 编写的项目,例如 Chrome 扩展或 Node.js,显然可以使用这些 API)。禁止使用非标准的语言“扩展”(例如,某些外部转译器提供的扩展)。

5.11.5 原始类型的包装对象

永远不要在原始对象包装器(BooleanNumberStringSymbol)上使用 new,也不要将它们包含在类型注释中。

不允许

const /** Boolean */ x = new Boolean(false);
if (x) alert(typeof x);  // alerts 'object' - WAT?

包装器可以作为函数调用来强制转换(这比使用 + 或连接空字符串更好)或创建符号。

示例

const /** boolean */ x = Boolean(0);
if (!x) alert(typeof x);  // alerts 'boolean', as expected

5.11.6 修改内置对象

永远不要修改内置类型,无论是通过向其构造函数或其原型添加方法。避免依赖于执行此操作的库。请注意,JSCompiler 的运行时库将尽可能提供符合标准的 polyfill;其他任何内容都不得修改内置对象。

除非绝对必要(例如,第三方 API 要求),否则不要向全局对象添加符号。

5.11.7 调用构造函数时省略 ()

永远不要在没有使用括号 () 的情况下在 new 语句中调用构造函数。

不允许

new Foo;

请改用

new Foo();

省略括号可能会导致细微的错误。以下两行并不等效

new Foo().Bar();
new Foo.Bar();

6 命名

6.1 所有标识符通用的规则

标识符仅使用 ASCII 字母和数字,以及在少数情况下(如下所述),下划线和极少数(当 Angular 等框架要求时)美元符号。

在合理的范围内,尽可能使用具有描述性的名称。不要担心节省水平空间,因为让新的读者立即理解您的代码更为重要。不要使用模糊或项目之外的读者不熟悉的缩写,也不要通过删除单词中的字母来进行缩写。

errorCount          // No abbreviation.
dnsConnectionIndex  // Most people know what "DNS" stands for.
referrerUrl         // Ditto for "URL".
customerId          // "Id" is both ubiquitous and unlikely to be misunderstood.

不允许

n                   // Meaningless.
nErr                // Ambiguous abbreviation.
nCompConns          // Ambiguous abbreviation.
wgcConnections      // Only your group knows what this stands for.
pcReader            // Lots of things can be abbreviated "pc".
cstmrId             // Deletes internal letters.
kSecondsPerDay      // Do not use Hungarian notation.

例外:作用域在 10 行或更少行中的变量,包括不是导出 API 的一部分的参数,可以使用短(例如,单字母)变量名。

6.2 按标识符类型划分的规则

6.2.1 包名

包名全部为 lowerCamelCase。例如,my.exampleCode.deepSpace,但不能是 my.examplecode.deepspacemy.example_code.deep_space

例外:包名可以符合 TypeScript 基于路径的模式。这通常是所有小写字母,文件名中存在下划线。

6.2.2 类名

类、接口、记录和类型定义名称以 UpperCamelCase 编写。未导出的类只是局部变量:它们未标记为 @private

类型名称通常是名词或名词短语。例如,RequestImmutableViewVisibilityMode。此外,接口名称有时也可以是形容词或形容词短语(例如,Readable)。

6.2.3 方法名

方法名以 lowerCamelCase 编写。@private 方法的名称可以选择以尾随下划线结尾。

方法名通常是动词或动词短语。例如,sendMessagestop_。属性的 getter 和 setter 方法不是必需的,但如果使用它们,则应该命名为 getFoo(或者对于布尔值,可以选择 isFoohasFoo),或 setter 的 setFoo(value)

下划线也可以出现在 JsUnit 测试方法名称中,以分隔名称的逻辑组成部分。一种典型的模式是 test<MethodUnderTest>_<state>_<expectedOutcome>,例如 testPop_emptyStack_throws。没有命名测试方法的唯一正确方法。

6.2.4 枚举名

枚举名称以 UpperCamelCase 编写,类似于类,并且通常应该是单数名词。枚举中的单个项目以 CONSTANT_CASE 命名。

6.2.5 常量名

常量名称使用 CONSTANT_CASE:所有大写字母,单词用下划线分隔。没有理由使用尾随下划线命名常量,因为私有静态属性可以用(隐式私有)模块局部变量替换。

6.2.5.1 “常量”的定义

每个常量都是 @const 静态属性或模块局部 const 声明,但并非所有 @const 静态属性和模块局部 const 都是常量。在选择常量大小写之前,请考虑该字段是否真的感觉像是深度不可变常量。例如,如果该实例的任何可观察状态都可以更改,那么它几乎肯定不是常量。仅仅打算永远不改变对象通常是不够的。

示例

// Constants
const NUMBER = 5;
/** @const */ exports.NAMES = goog.debug.freeze(['Ed', 'Ann']);
/** @enum */ exports.SomeEnum = { ENUM_CONSTANT: 'value' };

// Not constants
let letVariable = 'non-const';

class MyClass {
  constructor() { /** @const {string} */ this.nonStatic = 'non-static'; }
};
/** @type {string} */
MyClass.staticButMutable = 'not @const, can be reassigned';

const /** Set<string> */ mutableCollection = new Set();

const /** MyImmutableContainer<SomeMutableType> */ stillMutable =
    new MyImmutableContainer(mutableInner);

const {Foo} = goog.require('my.foo');  // mirrors imported name

const logger = log.getLogger('loggers.are.not.immutable');

常量的名称通常是名词或名词短语。

6.2.5.2 局部别名

只要局部别名可以提高可读性,就应该使用它们来代替完全限定的名称。遵循与 goog.require 相同的规则(??),保留别名名称的最后一部分。别名也可以在函数中使用。别名必须是 const

示例

const staticHelper = importedNamespace.staticHelper;
const CONSTANT_NAME = ImportedClass.CONSTANT_NAME;
const {assert, assertInstanceof} = asserts;

6.2.6 非常量字段名称

非常量字段名称(静态或其他)以 lowerCamelCase 编写,私有字段可以选择使用尾随下划线。

这些名称通常是名词或名词短语。例如,computedValuesindex_

6.2.7 参数名

参数名称以 lowerCamelCase 编写。请注意,即使参数期望构造函数,这也适用。

单字符参数名称不应在公共方法中使用。

例外:当第三方框架要求时,参数名称可以以 $ 开头。此例外不适用于任何其他标识符(例如,局部变量或属性)。

6.2.8 局部变量名

局部变量名称以 lowerCamelCase 编写,但如上所述,模块局部(顶级)常量除外。函数范围内的常量仍然以 lowerCamelCase 命名。请注意,即使变量保存构造函数,也使用 lowerCamelCase

6.2.9 模板参数名称

模板参数名称应该是简洁的、单字或单字母的标识符,并且必须全部大写,例如 TYPETHIS

6.2.10 模块局部名称

未导出的模块局部名称是隐式私有的。它们未标记为 @private。这适用于类、函数、变量、常量、枚举和其他模块局部标识符。

6.3 Camel case:定义

有时,将英语短语转换为驼峰式大小写有多种合理的方式,例如当存在缩写或不寻常的结构(如 IPv6iOS)时。为了提高可预测性,Google 风格指定了以下(几乎)确定性方案。

从名称的散文形式开始

  1. 将短语转换为纯 ASCII 并删除任何撇号。例如,Müller's algorithm 可能会变成 Muellers algorithm
  2. 将此结果分成单词,按空格和任何剩余的标点符号(通常是连字符)拆分。
    1. 推荐:如果任何单词在常见用法中已经具有传统的驼峰式外观,则将其拆分为其组成部分(例如,AdWords 变成 ad words)。请注意,像 iOS 这样的词本身并不是真正的驼峰式大小写;它违背了任何约定,因此此建议不适用。
  3. 现在将所有内容转换为小写(包括首字母缩写),然后仅将以下内容的首字母大写
    1. …每个单词,产生 UpperCamelCase,或
    2. …除了第一个单词之外的每个单词,产生 lowerCamelCase
  4. 最后,将所有单词连接成一个标识符。

请注意,原始单词的大小写几乎完全被忽略。

lowerCamelCase 的示例

散文形式 正确 不正确
XML HTTP request xmlHttpRequest XMLHTTPRequest
new customer ID newCustomerId newCustomerID
inner stopwatch innerStopwatch innerStopWatch
supports IPv6 on iOS? supportsIpv6OnIos supportsIPv6OnIOS
YouTube importer youTubeImporter youtubeImporter*

*可接受,但不推荐。

对于 UpperCamelCase 的示例,将每个正确的 lowerCamelCase 示例的首字母大写。

注意:有些词在英语中是不确定地用连字符连接的:例如,nonemptynon-empty 都是正确的,因此方法名称 checkNonemptycheckNonEmpty 也都是正确的。

7 JSDoc

JSDoc 用于所有类、字段和方法。

7.1 一般形式

JSDoc 块的基本格式如本例所示

/**
 * Multiple lines of JSDoc text are written here,
 * wrapped normally.
 * @param {number} arg A number to do something to.
 */
function doSomething(arg) { … }

或在本单行示例中

/** @const @private {!Foo} A short bit of JSDoc. */
this.foo_ = foo;

如果单行注释溢出到多行,则必须使用多行样式,并在其自己的行上使用 /***/

许多工具从 JSDoc 注释中提取元数据以执行代码验证和优化。因此,这些注释必须格式良好。

7.2 Markdown

JSDoc 用 Markdown 编写,但必要时可以包含 HTML。

请注意,自动提取 JSDoc 的工具(例如 JsDossier)通常会忽略纯文本格式,因此如果您执行此操作

/**
 * Computes weight based on three factors:
 *   items sent
 *   items received
 *   last timestamp
 */

它会像这样出来

Computes weight based on three factors: items sent items received last timestamp

相反,编写一个 Markdown 列表

/**
 * Computes weight based on three factors:
 *
 *  - items sent
 *  - items received
 *  - last timestamp
 */

7.3 JSDoc 标签

Google 风格允许使用 JSDoc 标签的子集。有关完整列表,请参见 ??。大多数标签必须占据它们自己的行,并且该标签位于行的开头。

不允许

/**
 * The "param" tag must occupy its own line and may not be combined.
 * @param {number} left @param {number} right
 */
function add(left, right) { ... }

不需要任何额外数据的简单标签(例如 @private@const@final@export)可以与可选类型一起组合在同一行上(如果合适)。

/**
 * Place more complex annotations (like "implements" and "template")
 * on their own lines. Multiple simple tags (like "export" and "final")
 * may be combined in one line.
 * @export @final
 * @implements {Iterable<TYPE>}
 * @template TYPE
 */
class MyClass {
  /**
   * @param {!ObjType} obj Some object.
   * @param {number=} num An optional number.
   */
  constructor(obj, num = 42) {
    /** @private @const {!Array<!ObjType|number>} */
    this.data_ = [obj, num];
  }
}

对于何时组合标签或以何种顺序组合标签没有硬性规定,但要保持一致。

有关注释 JavaScript 中类型的更多信息,请参见 注释 JavaScript 以用于 Closure CompilerClosure 类型系统中的类型

7.4 换行

换行的块标记缩进四个空格。换行的描述文本可以与前几行的描述对齐,但不鼓励这种水平对齐。

/**
 * Illustrates line wrapping for long param/return descriptions.
 * @param {string} foo This is a param with a description too long to fit in
 *     one line.
 * @return {number} This returns something that has a description too long to
 *     fit in one line.
 */
exports.method = function(foo) {
  return 5;
};

包装 @desc@fileoverview 描述时,不要缩进。

7.5 顶部/文件级别注释

一个文件可以有一个顶层文件概述。版权声明、作者信息和默认可见性级别是可选的。通常建议在文件包含多个类定义时使用文件概述。顶层注释旨在帮助不熟悉代码的读者了解此文件中的内容。如果存在,它可以提供文件内容的描述以及任何依赖关系或兼容性信息。包装的行不缩进。

示例

/**
 * @fileoverview Description of file, its uses and information
 * about its dependencies.
 * @package
 */

7.6 类注释

类、接口和记录必须使用描述以及任何模板参数、实现的接口、可见性或其他适当的标签进行文档化。类描述应为读者提供足够的信息,使其了解如何以及何时使用该类,以及正确使用该类所需的任何其他注意事项。构造函数可以省略文本描述。在定义类时,除非它扩展了泛型类,否则 @constructor@extends 注释不与 class 关键字一起使用。在定义 @interface@record 时,定义子类时使用 @extends 注释,并且永远不使用 extends 关键字。

/**
 * A fancier event target that does cool things.
 * @implements {Iterable<string>}
 */
class MyFancyTarget extends EventTarget {
  /**
   * @param {string} arg1 An argument that makes this more interesting.
   * @param {!Array<number>} arg2 List of numbers to be processed.
   */
  constructor(arg1, arg2) {
    // ...
  }
};

/**
 * Records are also helpful.
 * @extends {Iterator<TYPE>}
 * @record
 * @template TYPE
 */
class Listable {
  /** @return {TYPE} The next item in line to be returned. */
  next() {}
}

7.7 枚举和类型定义注释

所有枚举和类型定义都必须在前面的行中使用适当的 JSDoc 标签(@typedef@enum)进行文档化。公共枚举和类型定义也必须具有描述。可以使用前一行中的 JSDoc 注释来记录单个枚举项。

/**
 * A useful type union, which is reused often.
 * @typedef {!FruitType|!FruitTypeEnum}
 */
let CoolUnionType;
 
/**
 * Types of fruits.
 * @enum {string}
 */
const FruitTypeEnum = {
  /** This kind is very sour. */
  SOUR: 'sour',
  /** The less-sour kind. */
  SWEET: 'sweet',
};

类型定义对于定义短记录类型或联合、复杂函数或泛型类型的别名非常有用。对于具有许多字段的记录类型,应避免使用类型定义,因为它们不允许记录单个字段,也不允许使用模板或递归引用。对于大型记录类型,最好使用 @record

7.8 方法和函数注释

在方法和命名函数中,必须记录参数和返回类型,即使在相同签名的 @override 中也是如此。 必要时应记录 this 类型。 如果函数没有非空的 return 语句,则可以省略返回类型。

如果方法、参数和返回描述(但不是类型)从方法的其余 JSDoc 或其签名中显而易见,则可以省略它们。

方法描述以描述方法作用的动词短语开头。 这个短语不是祈使句,而是以第三人称编写,就好像在它之前有一个隐含的 This method ... 一样。

如果一个方法覆盖了一个超类方法,它必须包含一个 @override 注释。 对于被覆盖的方法,即使超类方法中没有类型被细化,也必须显式指定所有 @param@return 注释。 这是为了与 TypeScript 对齐。

/** A class that does something. */
class SomeClass extends SomeBaseClass {
  /**
   * Operates on an instance of MyClass and returns something.
   * @param {!MyClass} obj An object that for some reason needs detailed
   *     explanation that spans multiple lines.
   * @param {!OtherClass} obviousOtherClass
   * @return {boolean} Whether something occurred.
   */
  someMethod(obj, obviousOtherClass) { ... }

  /**
   * @param {string} param
   * @return {string}
   * @override
   */
  overriddenMethod(param) { ... }
}

/**
 * Demonstrates how top-level functions follow the same rules.  This one
 * makes an array.
 * @param {TYPE} arg
 * @return {!Array<TYPE>}
 * @template TYPE
 */
function makeArray(arg) { ... }

如果您只需要记录函数的参数和返回类型,您可以选择在函数的签名中使用内联 JSDoc。 这些内联 JSDoc 指定返回和参数类型,而无需标签。

function /** string */ foo(/** number */ arg) {...}

如果您需要描述或标签,请在方法上方使用单个 JSDoc 注释。 例如,返回值的方法需要一个 @return 标签。

class MyClass {
  /**
   * @param {number} arg
   * @return {string}
   */
  bar(arg) {...}
}
// Illegal inline JSDocs.

class MyClass {
  /** @return {string} */ foo() {...}
}

/** No function description allowed inline here. */ function bar() {...}

function /** Function description is also illegal here. */ baz() {...}

在匿名函数中,注解通常是可选的。如果自动类型推断不足或显式注解提高了可读性,则像这样注解参数和返回类型

promise.then(
    /** @return {string} */
    (/** !Array<string> */ items) => {
      doSomethingWith(items);
      return items[0];
    });

对于函数类型表达式,请参阅 ??

7.9 属性注释

必须记录属性类型。如果名称和类型提供了足够的文档来理解代码,则可以省略私有属性的描述。

公共导出的常量以与属性相同的方式进行注释。

/** My class. */
class MyClass {
  /** @param {string=} someString */
  constructor(someString = 'default string') {
    /** @private @const {string} */
    this.someString_ = someString;

    /** @private @const {!OtherType} */
    this.someOtherThing_ = functionThatReturnsAThing();

    /**
     * Maximum number of things per pane.
     * @type {number}
     */
    this.someProperty = 4;
  }
}

/**
 * The number of times we'll try before giving up.
 * @const {number}
 */
MyClass.RETRY_COUNT = 33;

7.10 类型注释

类型注释可在 @param@return@this@type 标签上找到,并且可选地可在 @const@export 和任何可见性标签上找到。附加到 JSDoc 标签的类型注释必须始终用大括号括起来。

7.10.1 可空性

类型系统定义了修饰符 !? 分别用于非空和可空。这些修饰符必须位于类型之前。

可空性修饰符对不同类型有不同的要求,这些类型大致分为两类

  1. 原始类型(stringnumberbooleansymbolundefinednull)和字面量({function(...): ...}{{foo: string...}})的类型注释默认始终是非空的。 使用 ? 修饰符使其可空,但省略冗余的 !
  2. 引用类型(通常,UpperCamelCase 中的任何内容,包括 some.namespace.ReferenceType)指的是在其他地方定义的类、枚举、记录或类型定义。 由于这些类型可能是可空的,也可能不是可空的,因此仅从名称上无法判断它是可空的还是不可空的。 始终对这些类型使用显式的 ?! 修饰符,以防止在使用位置出现歧义。

错误

const /** MyObject */ myObject = null; // Non-primitive types must be annotated.
const /** !number */ someNum = 5; // Primitives are non-nullable by default.
const /** number? */ someNullableNum = null; // ? should precede the type.
const /** !{foo: string, bar: number} */ record = ...; // Already non-nullable.
const /** MyTypeDef */ def = ...; // Not sure if MyTypeDef is nullable.

// Not sure if object (nullable), enum (non-nullable, unless otherwise
// specified), or typedef (depends on definition).
const /** SomeCamelCaseName */ n = ...;

正确

const /** ?MyObject */ myObject = null;
const /** number */ someNum = 5;
const /** ?number */ someNullableNum = null;
const /** {foo: string, bar: number} */ record = ...;
const /** !MyTypeDef */ def = ...;
const /** ?SomeCamelCaseName */ n = ...;

7.10.2 类型转换

如果编译器无法准确推断表达式的类型,并且 goog.asserts 中的断言函数无法补救它,则可以通过添加类型注释注释并将表达式括在括号中来收紧类型。 请注意,括号是必需的。

/** @type {number} */ (x)

7.10.3 模板参数类型

始终指定模板参数。 这样编译器可以更好地完成工作,并且使读者更容易理解代码的作用。

错误

const /** !Object */ users = {};
const /** !Array */ books = [];
const /** !Promise */ response = ...;

正确

const /** !Object<string, !User> */ users = {};
const /** !Array<string> */ books = [];
const /** !Promise<!Response> */ response = ...;

const /** !Promise<undefined> */ thisPromiseReturnsNothingButParameterIsStillUseful = ...;
const /** !Object<string, *> */ mapOfEverything = {};

不应使用模板参数的情况

7.10.4 函数类型表达式

术语说明函数类型表达式指的是函数类型的类型注释,注释中使用关键字 function(请参见下面的示例)。

在给出函数定义的情况下,不要使用函数类型表达式。使用 @param@return 或使用内联注释指定参数和返回类型(请参阅 ??)。 这包括匿名函数和已定义并分配给 const 的函数(函数 jsdoc 出现在整个赋值表达式上方)。

例如,在 @typedef@param@return 中需要函数类型表达式。如果函数类型的变量或属性没有立即用函数定义初始化,也请使用它。

  /** @private {function(string): string} */
  this.idGenerator_ = googFunctions.identity;

使用函数类型表达式时,始终显式指定返回类型。 否则,默认返回类型为 unknown (?),这会导致奇怪和意外的行为,并且很少是实际想要的。

错误 - 类型错误,但未给出警告

/** @param {function()} generateNumber */
function foo(generateNumber) {
  const /** number */ x = generateNumber();  // No compile-time type error here.
}

foo(() => 'clearly not a number');

正确

/**
 * @param {function(): *} inputFunction1 Can return any type.
 * @param {function(): undefined} inputFunction2 Definitely doesn't return
 *      anything.
 * NOTE: the return type of `foo` itself is safely implied to be {undefined}.
 */
function foo(inputFunction1, inputFunction2) {...}

7.10.5 空格

在类型注释中,每个逗号或冒号后都需要一个空格或换行符。 可以插入额外的换行符以提高可读性或避免超出列限制。 这些换行符应按照适用的准则进行选择和缩进(例如,????)。 类型注释中不允许使用其他空格。

正确

/** @type {function(string): number} */

/** @type {{foo: number, bar: number}} */

/** @type {number|string} */

/** @type {!Object<string, string>} */

/** @type {function(this: Object<string, string>, number): string} */

/**
 * @type {function(
 *     !SuperDuperReallyReallyLongTypedefThatForcesTheLineBreak,
 *     !OtherVeryLongTypedef): string}
 */

/**
 * @type {!SuperDuperReallyReallyLongTypedefThatForcesTheLineBreak|
 *     !OtherVeryLongTypedef}
 */

错误

// Only put a space after the colon
/** @type {function(string) : number} */

// Put spaces after colons and commas
/** @type {{foo:number,bar:number}} */

// No space in union types
/** @type {number | string} */

7.11 可见性注释

可以在 @fileoverview 块中或任何导出的符号或属性上指定可见性注释(@private@package@protected)。 不要为局部变量指定可见性,无论是在函数内还是在模块的顶层。 @private 名称可以选择以一个下划线结尾。

8 政策

8.1 Google 风格未指定的 Issue:保持一致!

对于本规范未明确解决的任何样式问题,最好执行与同一文件中其他代码已在执行的操作。如果这不能解决问题,请考虑模拟同一包中的其他文件。

8.2 编译器警告

8.2.1 使用标准警告集

在尽可能的情况下,项目应使用 --warning_level=VERBOSE

8.2.2 如何处理警告

在执行任何操作之前,请确保您准确了解警告告诉您的内容。如果您不确定为什么会出现警告,请寻求帮助。

了解警告后,请按顺序尝试以下解决方案

  1. 首先,修复它或解决它。 强烈尝试实际解决警告,或找到另一种完全避免这种情况的方式来完成任务。
  2. 否则,确定它是否为误报。 如果您确信警告无效,并且代码实际上是安全且正确的,请添加一条注释以说服读者相信此事实,并应用 @suppress 注释。
  3. 否则,请留下 TODO 注释。 这是一个 最后的手段。 如果您这样做,不要抑制警告。 警告应保持可见,直到可以正确处理为止。

8.2.3 在尽可能小的合理范围内抑制警告

警告在尽可能小的合理范围内被抑制,通常是单个局部变量或非常小的方法的范围。 通常,仅出于此原因提取变量或方法。

示例

/** @suppress {uselessCode} Unrecognized 'use asm' declaration */
function fn() {
  'use asm';
  return 0;
}

即使类中有大量的抑制,仍然比使整个类对此类警告视而不见要好。

8.3 弃用

使用 @deprecated 注释标记已弃用的方法、类或接口。 弃用注释必须包括简单、明确的说明,供人们修复其调用位置。

8.4 不符合 Google 风格的代码

您有时会在代码库中遇到不符合正确的 Google 风格的文件。 这些文件可能来自收购,或者可能是在 Google 风格对某些问题采取立场之前编写的,或者可能由于任何其他原因而不符合 Google 风格。

8.4.1 重新格式化现有代码

更新现有代码的样式时,请遵循以下准则。

  1. 不需要更改所有现有代码以满足当前的样式指南。 重新格式化现有代码是在代码变动和一致性之间的权衡。 样式规则会随着时间的推移而演变,并且这些类型的调整以保持合规性将产生不必要的变动。 但是,如果对文件进行了重大更改,则预计该文件将采用 Google 风格。
  2. 注意不要让随意的样式修改分散 CL 的重点。如果您发现自己进行了大量对 CL 的核心关注点并不重要的样式更改,请将这些更改提升到单独的 CL 中。

8.4.2 新增代码:使用 Google Style

无论同一包中其他文件的样式选择如何,全新的文件都应使用 Google Style。

当向不是 Google Style 的文件中添加新代码时,建议首先重新格式化现有代码,但应遵守 ?? 中的建议。

如果未进行此重新格式化,则新代码应尽可能与同一文件中的现有代码保持一致,但不得违反样式指南。

8.5 本地样式规则

团队和项目可以在本文档中的规则之外采用其他样式规则,但必须接受清理更改可能不遵守这些附加规则,并且不得因违反任何附加规则而阻止此类清理更改。 注意避免使用没有意义的过度规则。 样式指南并不试图定义每种可能情况下的样式,您也不应该这样做。

8.6 生成的代码:主要免除

构建过程生成的源代码不需要符合 Google Style。 但是,任何将从手写源代码引用的生成的标识符必须遵循命名要求。 作为一种特殊例外,允许此类标识符包含下划线,这可能有助于避免与手写标识符冲突。

9 附录

9.1 JSDoc 标签参考

JSDoc 在 JavaScript 中有多种用途。 除了用于生成文档外,它还用于控制工具。 最著名的是 Closure Compiler 类型注解。

9.1.1 类型注解和其他 Closure Compiler 注解

Closure Compiler 使用的 JSDoc 文档在 为 Closure Compiler 注释 JavaScriptClosure 类型系统中的类型 中进行了描述。

9.1.2 文档注解

除了 为 Closure Compiler 注释 JavaScript 中描述的 JSDoc 之外,以下标签也很常见,并且受到各种文档生成工具(例如 JsDossier)的良好支持,仅用于文档目的。

9.1.2.1 @author@owner - 不推荐使用。

不推荐使用。

语法:@author username@google.com (First Last)

/**
 * @fileoverview Utilities for handling textareas.
 * @author kuth@google.com (Uthur Pendragon)
 */

记录文件作者或测试所有者,通常仅在 @fileoverview 注释中使用。 单元测试仪表板使用 @owner 标签来确定谁拥有测试结果。

9.1.2.2 @bug

语法:@bug bugnumber

/** @bug 1234567 */
function testSomething() {
  // …
}

/**
 * @bug 1234568
 * @bug 1234569
 */
function testTwoBugs() {
  // …
}

指示给定的测试函数回归测试哪些错误。

多个错误应各自拥有自己的 @bug 行,以使搜索回归测试尽可能容易。

9.1.2.3 @code - 已弃用。 请勿使用。

已弃用。 请勿使用。 请改用 Markdown 反引号。

语法:{@code ...}

从历史上看,`BatchItem` 被写成 {@code BatchItem}

/** Processes pending `BatchItem` instances. */
function processBatchItems() {}

指示 JSDoc 描述中的术语是代码,因此可以在生成的文档中正确格式化。

9.1.2.4 @desc

语法:@desc Message description

/** @desc Notifying a user that their account has been created. */
exports.MSG_ACCOUNT_CREATED = goog.getMsg(
    'Your account has been successfully created.');
9.1.2.5 @link

语法:{@link ...}

此标签用于在生成的文档中生成交叉引用链接。

/** Processes pending {@link BatchItem} instances. */
function processBatchItems() {}

历史记录: @link 标签也已用于在生成的文档中创建外部链接。 对于外部链接,请始终使用 Markdown 的链接语法。

/**
 * This class implements a useful subset of the
 * [native Event interface](https://dom.spec.whatwg.org/#event).
 */
class ApplicationEvent {}
9.1.2.6 @see

语法:@see Link

/**
 * Adds a single item, recklessly.
 * @see #addSafely
 * @see goog.Collect
 * @see goog.RecklessAdder#add
 */

引用对另一个类函数或方法的查找。

9.1.2.7 @supported

语法:@supported Description

/**
 * @fileoverview Event Manager
 * Provides an abstracted interface to the browsers' event systems.
 * @supported IE10+, Chrome, Safari
 */

在 fileoverview 中使用,以指示该文件支持哪些浏览器。

您还可能在第三方代码中看到其他类型的 JSDoc 注解。 这些注解出现在 JSDoc Toolkit Tag Reference 中,但不被视为有效的 Google 样式的一部分。

9.1.3 框架特定注解

以下注解特定于特定框架。

9.1.3.1 Angular 1 的 @ngInject
9.1.3.2 Polymer 的 @polymerBehavior

https://github.com/google/closure-compiler/wiki/Polymer-Pass

9.1.4 关于标准 Closure Compiler 注解的说明

以下标签过去是标准的,但现在已弃用。

9.1.4.1 @expose - 已弃用。 请勿使用。

已弃用。 请勿使用。 请改用 @export 和/或 @nocollapse

9.1.4.2 @inheritDoc - 已弃用。 请勿使用。

已弃用。 请勿使用。 请改用 @override

9.2 常见的误解的样式规则

以下是关于 JavaScript 的 Google Style 中鲜为人知或常被误解的事实的集合。(以下是真实的陈述;这不是一份 误解 列表。)

存在以下工具来支持 Google Style 的各个方面。

9.3.1 Closure Compiler

此程序执行类型检查和其他检查、优化和其他转换(例如,将代码降级为 ECMAScript 5)。

9.3.2 clang-format

此程序将 JavaScript 源代码重新格式化为 Google Style,并且还遵循许多非必需但经常提高可读性的格式化实践。 clang-format 生成的输出符合样式指南。

不需要使用 clang-format。 允许作者更改其输出,并且允许审阅者要求进行此类更改; 争议以通常的方式解决。 但是,子树可以选择在本地选择加入此类强制执行。

9.3.3 Closure compiler linter

此程序检查各种错误和反模式。

9.3.4 Conformance framework

JS Conformance Framework 是 Closure Compiler 的一部分,它为开发人员提供了一种简单的方法来指定一组额外的检查,以针对他们的代码库运行,超出标准检查范围。 例如,一致性检查可以禁止访问某个属性,或调用某个函数,或缺少类型信息(未知)。

这些规则通常用于强制执行关键限制(例如定义全局变量,这可能会破坏代码库)和安全模式(例如使用 eval 或分配给 innerHTML),或者更宽松地改进代码质量。

有关更多信息,请参阅 JS Conformance Framework 的官方文档。

9.4 遗留平台的例外情况

9.4.1 概述

本节介绍当代码作者无法使用现代 ECMAScript 语法时应遵循的例外情况和其他规则。 当无法使用现代 ECMAScript 语法时,需要对推荐样式进行例外处理,并在此处进行概述。

9.4.2 使用 var

9.4.2.1 var 声明不是块作用域的

var 声明的作用域限定为最近的封闭函数、脚本或模块的开头,这可能会导致意外行为,尤其是在引用循环内部的 var 声明的函数闭包中。 以下代码给出了一个示例

for (var i = 0; i < 3; ++i) {
  var iteration = i;
  setTimeout(function() { console.log(iteration); }, i*1000);
}

// logs 2, 2, 2 -- NOT 0, 1, 2
// because `iteration` is function-scoped, not local to the loop.

9.4.2.2 尽可能靠近第一次使用处声明变量

即使 var 声明的作用域限定为封闭函数的开头,但出于可读性的目的,var 声明也应尽可能靠近其第一次使用处。 但是,如果变量在块外部被引用,则不要将 var 声明放在块内。 例如

function sillyFunction() {
  var count = 0;
  for (var x in y) {
    // "count" could be declared here, but don't do that.
    count++;
  }
  console.log(count + ' items in y');
}
9.4.2.3 使用 @const 作为常量变量

对于将使用 const 关键字的全局声明(如果可用),请改为使用 @const 注释 var 声明(对于局部变量,这是可选的)。

9.4.3 不要使用块作用域函数声明

不要这样做

if (x) {
  function foo() {}
}

虽然 ECMAScript 6 之前实现的大多数 JavaScript VM 都支持块内的函数声明,但它并未标准化。 实现彼此之间以及与现在标准的 ECMAScript 块作用域函数声明行为不一致。 ECMAScript 5 标准和之前的版本仅允许在脚本或函数的根语句列表中进行函数声明,并明确禁止在严格模式下的块作用域中使用它们。

为了获得一致的行为,请改用以函数表达式初始化的 var 在块内定义一个函数

if (x) {
  var foo = function() {};
}

9.4.4 使用 goog.provide/goog.require 进行依赖项管理

9.4.4.1 摘要

警告:goog.provide 依赖项管理已弃用。 所有新文件,即使在使用 goog.provide 管理旧文件的项目中,也应使用 goog.module。 以下规则仅适用于预先存在的 goog.provide 文件。

goog.provide 语句应分组在一起并首先放置。 所有 goog.require 语句应紧随其后。 这两个列表应用空行分隔。

与其他语言中的导入语句类似,即使它们超过 80 列的行长度限制,也应将 goog.providegoog.require 语句写在一行中。

这些行应按字母顺序排序,大写字母优先

goog.provide('namespace.MyClass');
goog.provide('namespace.helperFoo');

goog.require('an.extremelyLongNamespace.thatSomeoneThought.wouldBeNice.andNowItIsLonger.Than80Columns');
goog.require('goog.dom');
goog.require('goog.dom.TagName');
goog.require('goog.dom.classes');
goog.require('goog.dominoes');

在类上定义的所有成员都应在同一文件中。 只有顶级类才应在包含在同一类上定义的多个成员(例如,枚举、内部类等)的文件中提供。

这样做

goog.provide('namespace.MyClass');

不要这样做

goog.provide('namespace.MyClass');
goog.provide('namespace.MyClass.CONSTANT');
goog.provide('namespace.MyClass.Enum');
goog.provide('namespace.MyClass.InnerClass');
goog.provide('namespace.MyClass.TypeDef');
goog.provide('namespace.MyClass.staticMethod');

命名空间中的成员也可以被提供

goog.provide('foo.bar');
goog.provide('foo.bar.CONSTANT');
goog.provide('foo.bar.method');
9.4.4.2 使用 goog.scope 创建别名

警告: goog.scope 已弃用。 新文件不应使用 goog.scope,即使在现有项目中有 goog.scope 用法的情况下。

goog.scope 可以用来缩短在使用 goog.provide/goog.require 依赖管理的代码中对命名空间符号的引用。

每个文件只能添加一个 goog.scope 调用。 始终将其放置在全局作用域中。

开头的 goog.scope(function() { 调用之前必须只有一个空行,并且位于任何 goog.provide 语句、goog.require 语句或顶级注释之后。 调用必须在文件的最后一行关闭。 将 // goog.scope 附加到作用域的关闭语句。 用两个空格将注释与分号隔开。

与 C++ 命名空间类似,不要在 goog.scope 声明下缩进。 相反,从第 0 列开始。

仅为不会重新分配给另一个对象的名称创建别名(例如,大多数构造函数、枚举和命名空间)。 不要这样做(请参见下文了解如何别名构造函数)

goog.scope(function() {
var Button = goog.ui.Button;

Button = function() { ... };
...

名称必须与它们所别名的全局对象的最后一个属性相同。

goog.provide('my.module.SomeType');

goog.require('goog.dom');
goog.require('goog.ui.Button');

goog.scope(function() {
var Button = goog.ui.Button;
var dom = goog.dom;

// Alias new types after the constructor declaration.
my.module.SomeType = function() { ... };
var SomeType = my.module.SomeType;

// Declare methods on the prototype as usual:
SomeType.prototype.findButton = function() {
  // Button as aliased above.
  this.button = new Button(dom.getElement('my-button'));
};
...
});  // goog.scope
9.4.4.3 goog.forwardDeclare

优先使用 goog.requireType 而不是 goog.forwardDeclare 来打破同一库中文件之间的循环依赖关系。 与 goog.require 不同,允许 goog.requireType 语句在定义命名空间之前导入它。

goog.forwardDeclare 语句必须遵循与 goog.requiregoog.requireType 相同的样式规则。 整个 goog.forwardDeclaregoog.requiregoog.requireType 语句块按字母顺序排序。

goog.forwardDeclare 用于旧代码中,以打破*跨库边界*的循环引用。 但是,这种模式在构建工具中的支持很差,不应使用。 应组织代码以避免跨库的循环依赖关系(通过拆分/合并库)。

9.4.4.4 goog.module.get(name)

如果 goog.provide 文件依赖于 goog.module 文件,则 goog.provide 文件通常无法通过全局名称引用模块的导出。 相反,除了 goog.require() 模块之外,goog.provide 文件还必须通过调用 goog.module.get('module.name') 来获取模块的导出对象。

注意:仅调用 goog.module.get('module.name') 不会在构建时创建代码对模块的依赖关系。 构建依赖项需要 goog.require

9.4.4.5 goog.module.declareLegacyNamespace()

警告:goog.module.declareLegacyNamespace 仅用于过渡用途。

goog.module.declareLegacyNamespace 仅在将 JavaScript 文件及其使用者从 goog.provide 迁移到 goog.module 时使用。 更新您的 goog.module 的使用者以使用他们自己的 goog.module。 尽可能删除对 goog.module.declareLegacyNamespace 的调用。

如果您无法很快将旧命名空间的使用者从 goog.provide 更新到 goog.module,请将文件的内容包装在对 goog.scope 的调用中,使用 goog.module.get 导入旧命名空间,然后在您的 goog.module 中删除对 goog.module.declareLegacyNamespace 的调用。

goog.module(name) 中调用 goog.module.declareLegacyNamespace() 会将模块的命名空间声明为全局名称,就像 goog.provide() 调用一样。 这允许非 goog.module 命名空间访问模块的导出,而无需调用 goog.module.get(name)