资讯专栏INFORMATION COLUMN

翻译谷歌代码风格之JavaScript篇(未完待续)

duan199226 / 2102人阅读

摘要:综述此文档为谷歌基于代码风格的完整定义。只有一篇文件遵守了以下规则的情况下,此文件可以被称为遵从谷歌代码风格。谷歌命名空间继承关系声明谷歌模块声明后可以再声明命名空间继承关系。

1.综述

此文档为谷歌基于JavaScript代码风格的完整定义。只有一篇JavaScript文件遵守了以下规则的情况下,此文件可以被称为遵从谷歌代码风格。
正如其他谷歌代码风格一样,本文的跨度不仅包括了代码格式的美观,同样也包括了代码标准以及惯例。

1.1 术语

在本文中,除特别注明之外:

术语“注释”一般指的是实现注释。我们不会使用“文档注释”,而作为代替,我们使用一般术语"JS注文(JSDoc)"说明在/** … */之中的人类可读以及机器可读的注释。

当用到"必须""必须不""应该""不应该"以及"可以"的时候,本文遵从RFC 2119术语规则。其中,术语中的倾向于避免应该不应该一致,而相应的,本文中命令与陈述式的说明则与术语中的必须相一致。

1.2 特别提示

文中所有的示例代码都是非标准的。换句话说,给出的谷歌代码风格示例代码都不是基于风格下实现这段代码的唯一方法。示例代码中给出的可选格式并不是需要强制执行的规则。

2.源文件基本规则 2.1 文件名

文件名必须小写,并且不可以含有除了下划线(_)和破折号(-)之外的标点符号。请与你的项目风格和习惯保持一致。文件名的后缀必须是.js

2.2 文件编码:UTF-8

所有源文件都必须在UTF-8编码下。

2.3 特殊字符 2.3.1 空白字符

除了换行符之外,在文件出现的空白字符只能是ASCII中的空格符(0x20)。这也就是说:

字符串中出现其他空白字符都会被转义

制表符(Tab)不能用于缩进

2.3.2 特殊转义字符

任何含有特殊转义字符的字符(", ", , b, f, n, r, t, v)总是优先于相应的数字转义字符(如x0a, u000a, u{a})。
永远不要使用早已被舍弃的八进制转义字符。

2.3.3 非ASCII码字符

对于剩余的非ASCII码字符,无论是实际编码字符(例如),16进制字符或是转义字符(例如u221e),只能在使代码可读性和可理解性更好的情况下使用。
提示:在使用转义字符的时候,或者甚至在使用实际编码字符的某些时候,在后面加上一个解释说明的注释会有助于代码的优雅和可读性。
例如:

 const units = "μs";(最优秀的写法,即使没有注释也很明确清晰)                
 const units = "u03bcs"; // "μs"(允许但是不被推荐)       
 const units = "u03bcs"; // Greek letter mu, "s"(允许但是简陋而又易于出错)        
 const units = "u03bcs";(失败的写法,使阅读者难以理解)      
 return "ufeff" + content; // byte order mark(很好的写法,用转义字符来表示非输出字符,后面必要时会有注释)    

提示:由于某些程序可能无法正确处理非ASCII码字符,所以永远不要让你代码因此而失去可读性。
同样,你的代码也会因此而失败需要修复。

3.源文件框架结构

一个多带带源文件必须依次含有以下信息:

许可与版权信息(如果需要)

文件总览JS注文(@fileoverview JSDoc)(如果需要)

谷歌模块(goog.module)声明

谷歌引入文件(goog.require)声明

文件正文

3.1 许可与版权信息(如果需要)

如果文件有许可与版权信息,必须写在文件结构的第一层。

3.2 文件总览JS注文(@fileoverview JSDoc)(如果需要)

具体格式见7.5节

3.3 谷歌模块(goog.module)声明

所有文件都必须在多带带的一行中声明作为谷歌模块的名字:含有谷歌模块命名的那一行不能换行,因此它会被排除在80列(字符)限制之外。
谷歌模块命名的全部就是定义一个命名空间。它作为这个包的名字(用来映射该源文件在整个代码资源目录里位置的标识),同样也可以代表该文件的主要类/枚举类型/接口。
例如:

*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.setTestOnly)

谷歌模块声明后可以再声明为测试专用(goog.setTestOnly())。

3.3.3 谷歌命名空间继承关系声明(goog.module.declareLegacyNamespac)

谷歌模块声明后可以再声明命名空间继承关系(goog.module.declareLegacyNamespace)。(尽量避免)
例如:

goog.module("my.test.helpers");    
goog.module.declareLegacyNamespace();       
goog.setTestOnly();

谷歌声明命名空间继承关系是用来简易地过渡传统面向对象层级关系命名,但是有一些命名上的限制。因为子系模块的命名一定是在父系模块之后,所以它一定不能是另一个谷歌模块的子系或者父系。(如:goog.module("parent);和goog.module("parent.child);同时存在会产生安全性问题goog.module("parent);goog.module("parent.child.grandchild")也会有同样的问题

3.3.4 ES6模块

因为到目前为止ES6模块的语言还没有完全完善,所以现在请不要使用ES6模块(例如,关键词exportimport)。注意,当ES6模块语言完全完善之后,就可以使用那些模块了。

3.4 谷歌引入声明(goog.require)

在模块声明之后,引入的模块通过谷歌引入(goog.require)来声明。每个声明的引入,都会分配一个固定的别名,或者拆分成几个固定的别名。在代码和注释里,除了声明引入的时候,这个别名是引入模块的唯一合法指代,引入模块的全名不能被使用。模块引入的别名尽可能地和该模块全名通过点"."拆分后的最后一部分的名字相一致,但是在能够避免歧义或者明显增加代码可读性的情况下,也可以加上模块全名的其他部分。
如果只是因为一个模块的副作用(代码意义的副作用,而非医学意义,可以理解为"本不应有或者用户意料之外的作用",或者简单理解为为了避免编译器警告[warning]的作用)而引入它,可以不用分配名字,但是在代码的其他地方不能出现该模块的全名。
引入声明先后排序规则:首先将带名字的引入声明按首字母排序列出,然后是解构(Destructuring)引入声明按首字母排序列出,最后再把剩余的引入声明多带带列出(一般是用到副作用的引入模块)。
提示:没有必要完全记住这个顺序,然后严格按照这个顺序排列。你可以根据你的IDE来排列你的引入声明,又是较长的模块名或者别名会违反80字符(列)限制,所以这一行不能换行。换句话说,引入声明的那几行不会有80字符(列)限制。
例子:

 const MyClass = goog.require("some.package.MyClass");
 const NsMyClass = goog.require("other.ns.MyClass");
 const googAsserts = goog.require("goog.asserts");
 const testingAsserts = goog.require("goog.testing.asserts");
 const than80columns = goog.require("pretend.this.is.longer.than80columns");
 const {clear, forEach, map} = goog.require("goog.array");
 /** @suppress {extraRequire} Initializes MyFramework. */
    goog.require("my.framework.initialization");

不被允许的写法:

const randomName = goog.require("something.else"); // 名字不匹配
const {clear, forEach, map} = // 不要换行 
          goog.require("goog.array");              
  function someFunction() {
          const alias = goog.require("my.long.name.alias"); // 必须在顶部(层)
          // …
    }
3.4.1 谷歌前置声明(goog.forwardDeclare

前置声明不常被用到,但是却是用来处理循环依赖和引用后期加载代码的有效方法。将所有前置声明一起写在引入声明之后。谷歌前置声明遵从和谷歌引入声明一样的规则。

3.5 文件的正文

文件的正文现在所有依赖文件以后(中间至少隔一行)。
其中可以包括任何模块内部声明(常亮,变量,类,函数等等)和已引入的符号。

4.格式

术语解释:块状结构指的是类,函数,方法或者任何被大括号包住的内容里的代码。值得注意的是,由于5.2小节和5.3小节的规定,数组和对象也可以被当成块状结构。
小提示:推荐使用clang-format工具。JavaScript社区已经成功完成了clang-format对JavaScript语言的支持,其中也集成了几位著名代码开发者的努力。

4.1 大括号(花括号) 4.1.1 流程控制结构需要使用大括号

所有流程控制结构(例如if, else, for, do, while等等)都需要使用大括号,哪怕其包含的主体代码只有一条指令。第一条指令的非空块状结构必须另起一行。
不被允许的写法:

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

例外情况:如果指令可以完全地用写在一行里,那么可以不用大括号以增加可读性。以下的例子是流程控制结构里唯一可以不用大括号和空行的例子:

if (shortCondition()) return;
4.1.2 非空区块:K&R风格

根据K&R风格对于非空区块和非空块状结构中大括号的规定:

左大括号(前一个大括号)之前不空行

左大括号后新起一行

右大括号(后一个大括号)前新起一行

在函数,类,类的方法定义的右大括号之后新起一行,而在else, catch, while,逗号,分好,右小括号之后的右大括号不空行。

例如:

class InnerClass {
          constructor() {}

      /** @param {number} foo */
      method(foo) {
        if (condition(foo)) {
          try {
            // Note: this might fail.
            something();
          } catch (err) {
            recover();
          }
        }
      }
    }
4.1.3 空区块:可简化

空区块和空块状结构开始之后可以直接结束,在{}中间不用任何字符,空格和换行,除非它是多区块结构中的一部分(比如if/else/try/catch/finally)。
例如:

    function doNothing() {}
不被允许的写法:
    if (condition) {
      // …
    } else if (otherCondition) {} else {
      // …
    }
    
    try {
      // …
    } catch (e) {}
4.2 区块缩进:两个空格

每当新开一个区块或块状结构,增加两个空格的缩进。区块结束之后,缩进恢复到前一级水平。缩进对该区块内的代码和注释同样有效(见4.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 对象声明:可作为块状结构

任何对象都可以按块状结构的格式书写,就像4.2.1节的例子。例如,以下的写法都是有效(不代表全部写法):

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 类的声明

类的声明(无论是内容声明[declarations]还是表达式声明[expressions])都像块状结构一样缩进。在类的方法声明和类中的内容声明(表达式结束时仍然需要加分号)的右大括号(后一个大括号)之后不加分号。其中可以使用关键字extends,但是不要用@extends的JS注文(JSDoc),除非你继承了一个模板类型(templatized type)。
例如:

class Foo {
  constructor() {
    /** @type {number} */
    this.x = 42;
  }

  /** @return {number} */
  method() {
    return this.x;
  }
}
Foo.Empty = class {};
/** @extends {Foo} */
foo.Bar = class extends Foo {
  /** @override */
  method() {
    return super.method() / 2;
  }
};

/** @interface */
class Frobnicator {
  /** @param {string} message */
  frobnicate(message) {}
}
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语句

就像其他块状结构,该语句的缩进方式也是+2。
开始新的一条Switch标签,格式要像开始一个新的块状结构,新起一行,缩进+2。适当时候可以用块状结构来明确Switch全文范围。
而到下一条Switch标签的开始行,缩进(暂时)还原到原缩进水平。
break和下一条Switch标签之间可以适当地空一行。
例子:

 
 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字符的限制。除了下面列出的例外情况之外,每行的字符超过80个就会自动换行(具体规则见4.5节)
例外情况:

该行条件上不支持80字符限制的可能(一个很长的url,JS注文或者复制黏贴来的shell命令)

谷歌模块(goog.module)和谷歌引入(goog.require)的声明(见3.3节和3.4节)

4.5 自动换行(Line-wrapping
术语解释:自动换行是指将一行的代码分成几行。   

没有固定的自动换行方法。通常来讲,对于同一代码有好几种合法的自动换行的方法。
注意:虽然官方上自动换行的目的是规避每行的字符限制,但是在不违反字符限制的情况,文件作者也可以根据自己的判断来自动换行。
小提示:精简方法或者变量可以避免自动换行。

4.5.1 断句的位置

自动换行的第一要务:在更高语言优先级的位置断句
更好的写法:

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

不够优秀的写法:

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

在上面的例子中,语言优先级从高到底依次排列:表达式,除号,函数调用,参数,数字。
运算符换行规则:

请在运算符之后换行(注意这和JAVA谷歌代码风格不同)。“.”并不是一个运算符,所以不适用上述规则。

方法和构造函数之后的左圆括号不能换行。

逗号紧跟前面的代码。

注意:自动换行的主要目的是使代码更清晰,代码不一定要越短越好。

4.5.2 自动换行新的一行缩进至少+4

除非根据缩进规则特别规定之外,自动换行后新的一行在原有的缩进水平上至少增加4个空格。
如果自动换行了多行,则可以适当的调整缩进水平。一般来讲,自动换行新的一行缩进会基于4的倍数。只有同一层次的两行才会用同样的缩进水平。

4.5.3节列出了一个使用变量空间来对齐的不被推荐的写法。 4.6 空白 4.6.1 垂直空白(空行)

在以下情况下会出现空行:

一个类或者对象声明中的两个函数或者方法之间。

例外情况:类中的两个属性声明之间(中间没有其他代码)可以选择性地空一行。这样做可以对属性进行逻辑分组。

方法中尽量少用空行的逻辑分组。函数主体的开头和结尾不能空行。

一个类或者对象声明中第一个方法之前或者最后一个方法之后可以选择性地空行(既不推荐也不反对)

文件其他需要的位置(例如3.4节所写的)

4.6.2 水平空白(空格)

水平空白根据出现的位置不同分为三类:头部,尾部,中间。头部的空白(例如缩进)在本文的其他部分都已经解释过了,而尾部的空白禁止出现。
除了其他规则特别规定以及常量,注释,JS注文之外,中间部分的水平空白只可以在下列位置出现:

用来分隔保留关键词(例如if, for, catch)和之后的左圆括号(()。

用来分隔右花括号(})和保留关键词(例如else, catch)。

左花括号({)之前,但是有两个例外:

a.数组字面量中一个函数的参数或者元素是对象字面量(例如foo({a: [{c:b.}])。
b.模板扩张中(例如abc${1 + 2}def。

在二元或者三元运算符的两边

在逗号和分号之后。注意,逗号和分号之前不能空格。

在对象字面量中的冒号之后。

在标识注释的双斜杠(//)的两边。这里可以使用多个空格(但是不是必须的)。

在JS注文(JSDoc)的开始标志之后和结束标志的两边(比如在简易声明或者造型定义中:this.foo = /** @type {number} */ (ba);*或者function(/** string */ foo) {)。

4.6.3 水平对齐:不被鼓励的用法
术语解释:水平对齐是指在标记后加空格让它定位到之前的标记的正下方。

这种写法是允许的,但是谷歌代码风格不鼓励这种方法。在用到水平对齐的时候也甚至不需要保持使用它。
下面是一个不使用对齐和使用了对齐的例子,而后者是不被鼓励的用法。

{
  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字符,或者一个参数一行来增加可读性。缩进4个空格。允许和圆括号对齐,但是不推荐。
下列就是最常见的函数参数对齐模式:

// 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 分组括号(Grouping parentheses):推荐的写法

分组括号也可以不加,在代码编写者或评定者都觉得这样不会使代码产生歧义而且不会影响代码的可读性的情况下。因为我们不能肯定每个代码的阅读者都记住了运算符优先表。
delete, typeof, void, return, throw, case, in, of,或 yield的表达式之后不要加无用的圆括号。
类型造型时要加圆括号:/ @type {!Foo} / (foo)*。

4.8注释

这一节讲的是实现注释的规则。JS注文规则请见第7章。

4.8.1 块注释风格

块注释遵从上下文一致的缩进规则。它们可以用/* … */ 和 //。对于多行注释(/* … */),含有*的一行中的*必须与其他的对齐,以明确注释没有多出来的文本。如果方法和值的意义不明确,请在之后加上“参数名”注释。

/*
 * This is
 * okay.
 */

// And so
// is this.

/* This is fine, too. */

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

注释不要包含在带星号或者别的字符的框画(boxes drawn)中。
不要在实现注释中JS注文(/** … */)。

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 需要注释声明类型

在声明的这一行或者上一行加上JS类型注文
例如:

const /** !Array */ data = [];

/** @type {!Array} */
const data = [];
5.2 数组字面量 5.2.1 用逗号结尾

在最后一个元素和右方括号之间用逗号结尾,并分行。
例如:

const values = [
  "first value",
  "second value",
];
5.2.2 不要用变长数组构造函数(new)

当对代码进行修改时,这种构造函数很容易出错。
不被允许的写法:

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

第三个例子会出现异常:如果x1纯数字,a3就会是个共有x1undefined元素的数组,如果x1是个其他数字,系统会抛出异常,如果x1是个其他类型,a3会是个单元素的数组。
应该这么写:

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=} 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构造器相同的问题。但是出于一致性的考虑仍然不允许用Object构造器。在这里用对象字面量({}或者{a: 0, b: 1, c: 2})。

5.3.3 带引号键值和不带引号键值不要混用

结构体(含有不带引号键值或符号)和字符文本结构(含有计算属性或者带引号键值),在同一个对象字面量中不要混用两者。
不被允许的写法:

{
  a: 42, // struct-style unquoted key
  "b": 43, // dict-style quoted key
}
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"}),然后如果一个重构对象是它自己,必须默认设为{}。JS注文可以给重构参数一个名字(不会用到但是可以被编译器接受)。
例如:

/**
 * @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也可以重构,这一行不能换行。整个代码无论多长都必须放在一行(见3.4节)。

5.3.8 枚举类型

对象字面量加上@enum注释之后可以定义枚举类型。一旦定义完成后,就不能再给枚举类型增加属性了。枚举类型必须保持不变,枚举类型中的值也不能更改。

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

/**
 * An enum with two options.
 * @enum {number}
 */
const Option = {
  /** The option used shall have been the first. */
  FIRST_OPTION: 1,
  /** The second among two options. */
  SECOND_OPTION: 2,
};
5.4 类 5.4.1 构造

可以使用构造器来构造类。在设置域或者访问this之前构造子类必须调用super()。接口中不可以调用构造函数。

5.4.2 域

在构造器中设置所有实例的域(例如除了方法之外的所有属性)。永不要用@const再指定注释域。私有域必须带@private的注释,名字用下划线结尾。不要用类的prototype设置域。
例如:

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

注意:构造器构造完成之后不要给一个实例添加或移除属性,这样会大大降低VM优化效果。如果一个域在定义的时候没有初始化,那应该在构造器中把它设为undefined以防止之后类型转变。对象中增加@struct会检查未声明的属性不能访问和增加。类默认有这个属性。

5.4.3 计算属性

类中只有当属性是Symbol类型的时候才能使用计算属性。不允许使用文本属性(如5.3.3节中间定义,也就是带引号或者非符号的计算属性)。任何类都可以定义可迭代的[Symbol.iterator]的方法。
注意:使用其他内置Symbol的时候要注意不要被编译器填充导致其他浏览器失效。

5.4.4 静态方法

在不影响可读性的情况,我们更推荐使用模型内置局部方法,而不是私有静态方法。
静态方法应该只在基类中被访问。静态方法不能访问包含本构造器或者子类构造器(如果这么做,必须在定义含有@nocollapse)创建实例的变量,也不能在没有定义过该方法的子类中调用。
不被允许的写法:

class Base { /** @nocollapse */ static foo() {} }
class Sub extends Base {}
function callFoo(cls) { cls.foo(); }  // discouraged: don"t call static methods dynamically
Sub.foo();  // illegal: don"t call static methods on subclasses that don"t define it themselves
5.4.5 旧风格的类声明

尽管我们推荐ES6的类,但是有些情况下ES6的类不可用。
例如:

如果存在或者即将存在子类,包括创建子类的框架,就不能立刻转为ES6语法。如果这种情况下类使用了ES6语法,那么它下属的所有子类都要用ES6语法规范。

因为ES6在调用super返回值之前无法访问this实例,所以在调用超类构造器之前需要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;
};

如果有超类,应在超类构造器被调用之后在本构造器中定义实例的属性。方法应在构造器的prototype定义。
正确定义构造器的prototype层级是很困难的。所有,最好使用the Closure Library 中的goog.inherits

5.4.6 不要直接操作prototype

关键词class可以定义比prototype更清晰和可读性更好的类。常规实现代码不需要操作这些对象,尽管这样做可以定义5.4.5节中所说的@record接口和类。不允许混入和修改嵌入对象的prototype
例外:代码框架(例如Polymer或者Angular)有时需要用到prototype,以避免求助于更不推荐的工作区。
例外2:定义接口中的类(见5.4.9节)

5.4.7 getter函数与setter函数

请不要使用JavaScript中的getter函数与setter函数。它们会产生潜在的危险和困难,而且只被部分编译器支持。建议使用常规方法来代替。
例外:当使用数据封装的框架(例如Polymer或者Angular),可以少量地使用getter函数与setter函数。但是请注意,这些方法只被部分编译器支持。使用的时候请在数组或者对象字面量中加上get foo()或者set foo(value)定义,如果做不到这些,请加上Object.defineProperties。请不要使用会重命名接口属性的Object.defineProperty。getter函数一定不能改变显状态。
不被允许的写法:

class Foo {
  get next() { return this.nextId++; }
}
5.4.8 toString方法的覆盖

方法toString可以被覆盖,但是定义的方法一定要能无副作用的运行。
特别值得注意的是,在toString中调用其他方法有可能导致无限循环的异常情况。

5.4.9 接口

接口可以用@interface@record来声明。@record声明的接口可以被类和对象字面量显式实现(例如通过@implements),也可以隐式实现。
在接口中的所有非静态方法都必须为空区块。在接口主体之后必须定义域作为prototype上留的底。
例如:

/**
 * Something that can frobnicate.
 * @record
 */
class Frobnicator {
  /**
   * Performs the frobnication according to the given strategy.
   * @param {!FrobnicationStrategy} strategy
   */
  frobnicate(strategy) {}
}

/** @type {number} The number of attempts before giving up. */
Frobnicator.prototype.attempts;
5.5 函数 5.5.1 顶层函数

需要导出的函数可以直接定义在exports对象中,也可以局部定义然后多带带导出。我们推荐使用不导出的函数,不要加上@private定义。
例如:

/** @return {number} */
function helperFunction() {
  return 42;
}
/** @return {number} */
function exportedFunction() {
  return helperFunction() * 2;
}
/**
 * @param {string} arg
 * @return {number}
 */
function anotherExportedFunction(arg) {
  return helperFunction() / arg.length;
}
/** @const */
exports = {exportedFunction, anotherExportedFunction};
/** @param {string} arg */
exports.foo = (arg) => {
  // do some stuff ...
};
5.5.2 嵌套函数和闭包

函数中可以嵌套函数,如果需要给函数命名,必须局部const定义。

5.5.3 箭头函数

箭头函数语法简洁,而且弥补了使用this的很多问题。比起使用关键词function,我们更推荐箭头函数,特别适用于嵌套函数(见5.3.5节)。
比起f.bind(this),特别是goog.bind(f, this),我们倾向于使用箭头函数。避免const self = this的写法。箭头函数常常对于有可能会传参的回调很有效。
箭头的右边可以是个表达式和块状结构。如果只有一个多带带的非解构参数,那么参数的圆括号可以省略。
注意:因为如果后期增加函数的参数,省略圆括号会使代码出错,所以即使箭头函数只有一个参数,加上圆括号也是很好的做法。

5.5.4 生成器函数(Generator

生成器函数提供了一些有用的抽象方法,可以在需要的时候使用。
定义生成器函数时,在function后附上*,并与函数名空一格。当使用授权域的时候,在yield后加上*
例如:

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

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

class SomeClass {
  /** @return {!Iterator} */
  * gen() {
    yield 42;
  }
}
5.5.5 参数

函数参数必须在JS注文中预定义格式化,除非使用了@override,所有类型省略。
内联参数类型必须在参数名前特别说明(比如/ number / foo, / string / bar) => foo + bar)。内联类型注释和@param类型注释在同一个函数声明时不能混用。

5.5.5.1 默认参数(缺省参数)

在参数列中可选参数允许使用等号操作符。就像必须参数一样写可选参数(比如不加opt_前缀),等号两边需加空格。在JS注文可选参数必须在类型声明中加上=后缀,不要用初始化以确保代码明确。就算可选参数的默认值是undefined,也要在函数声明中声明默认值。
例如:

/**
 * @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) {}

尽量少用默认参数。当有较多非自然语序可选参数时,我们更推荐重构(见5.3.7节)来创建可读性更好的API。
注意:不同于python的缺省参数,允许使用返回新的可变对象(比如{}和[])的初始化模块因为它会预先设定每次都使用默认值,所以调用之间对象不会共享。
小提示:包括函数调用的任何表达式都会用到初始化模块,所以初始化模块应该尽量简单。避免初始化模块暴露共享可变域,这容易导致函数调用之间的无意耦合。

5.5.5.2 剩余参数(定参数)

用剩余参数来代替访问arguments。在JS注文中剩余参数需要加一个...前缀。剩余参数必须在参数列表的最后。在参数名和...之间不要空格。不要把它命名成var_args,也不要用arguments来命名参数或局部变量,以避免和内置名的混淆。
例如:

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

在函数声明之前的JS注文中必须明确定义返回值类型,除非是@override情况下所有类型都省略。

5.5.7 泛型

定义泛型函数或方法时需在JS注文中加上@template TYPE

5.5.8 展开运算符

函数调用时可以使用展开运算符(...)。一个可变函数中一个数组或者它的迭代被分配成了多个参数时,比起Function.prototype.apply,我们更推荐展开运算符。
例如:

function myFunction(...elements) {}
myFunction(...array, ...iterable, ...generator());
5.6 字符串字面量 5.6.1 单引号

通常情况下,比起双引号,我们更推荐单引号来修饰字符串字面量。
小提示:如果字符串中含有单引号,考虑使用模板字符串来避免解析错误。
通常情况下,字符串不能跨行。

5.6.2 模板字符串

使用模板字符串来处理复杂的字符串拼接,尤其当处理多条字符串字面量时。

           
               
                                           
                       
                 

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/92613.html

相关文章

  • 自己撸个简单的ps切图脚本(未完待续…)

    摘要:刚做完的一个项目里,为了切图方便,接触了下的脚本功能。这里解释一下,也就是本文所讨论的脚本,并不只有可以用,都是可以用的,不过需要调用各自不同的。一些脚本的线上参考资料常用部分汉化版常数表初识脚本留坑待续 刚做完的一个H5项目里,为了切图方便,接触了下Photoshop的脚本功能。从找资料、写脚本到实际能用全套跑了一圈下来发现,嗯,果然是挺难用的[捂脸]。不过虽然缺点满满,但PS这个平...

    draveness 评论0 收藏0
  • 再遇设计模式JavaScript

    摘要:在面向对象的语言中,比如,等,单例模式通常是定义类时将构造函数设为,保证对象不能在外部被出来,同时给类定义一个静态的方法,用来获取或者创建这个唯一的实例。 万事开头难,作为正经历菜鸟赛季的前端player,已经忘记第一次告诉自己要写一些东西出来是多久以的事情了。。。如果,你也和我一样,那就像我一样,从现在开始,从看到这篇文章开始,打开电脑,敲下你的第一篇文章(或者任何形式的文字)吧。 ...

    Clect 评论0 收藏0
  • Android——processes,thread,AsyncTask(未完待续。)

    摘要:但我们还得遵守两个原则不让主线程阻塞。不在主线程线程外的线程进行的创建和修改。我们只需在主线程调用方法即可使用。 由于没有学过操作系统。所以给这process,thread,AsyncTask三个搞糊涂了。 process老师没讲过,thread和AsyncTask只是讲了下使用。至于为什么要用AsyncTask也没多解释。 今天看了下Processes and Thread,安卓...

    Anchorer 评论0 收藏0
  • 【译】node js event loop part 1.1

    原文 先说1.1总揽: Reactor模式 Reactor模式中的协调机制Event Loop Reactor模式中的事件分离器Event Demultiplexer 一些Event Demultiplexer处理不了的复杂I/O接口比如File I/O、DNS等 复杂I/O的解决方案 未完待续 前言 nodejs和其他编程平台的区别在于如何去处理I/O接口,我们听一个人介绍nodejs,总是...

    macg0406 评论0 收藏0
  • 前端开发知识点整理

    摘要:前言本文主要是有关前端方面知识按照目前的认知进行的收集归类概括和整理,涵盖前端理论与前端实践两方面。 前言:本文主要是有关前端方面知识按照 XX 目前的认知进行的收集、归类、概括和整理,涵盖『前端理论』与『前端实践』两方面。本文会告诉你前端需要了解的知识大致有什么,看上去有很多,但具体你要学什么,还是要 follow your heart & follow your BOSS。 初衷...

    Blackjun 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<