diff --git a/es-next-style-guide.md b/es-next-style-guide.md index df6000c..27082cb 100644 --- a/es-next-style-guide.md +++ b/es-next-style-guide.md @@ -1,58 +1,10 @@ -# JavaScript 编码规范 - ESNext 补充篇(草案) - - - - -[1 前言](#user-content-1-%E5%89%8D%E8%A8%80) - -[2 代码风格](#user-content-2-%E4%BB%A3%E7%A0%81%E9%A3%8E%E6%A0%BC) - -  [2.1 文件](#user-content-21-%E6%96%87%E4%BB%B6) - -  [2.2 结构](#user-content-22-%E7%BB%93%E6%9E%84) - -    [2.2.1 缩进](#user-content-221-%E7%BC%A9%E8%BF%9B) - -    [2.2.2 空格](#user-content-222-%E7%A9%BA%E6%A0%BC) - -    [2.2.3 语句](#user-content-223-%E8%AF%AD%E5%8F%A5) - -[3 语言特性](#user-content-3-%E8%AF%AD%E8%A8%80%E7%89%B9%E6%80%A7) - -  [3.1 变量](#user-content-31-%E5%8F%98%E9%87%8F) - -  [3.2 解构](#user-content-32-%E8%A7%A3%E6%9E%84) - -  [3.3 模板字符串](#user-content-33-%E6%A8%A1%E6%9D%BF%E5%AD%97%E7%AC%A6%E4%B8%B2) - -  [3.4 函数](#user-content-34-%E5%87%BD%E6%95%B0) - -  [3.5 箭头函数](#user-content-35-%E7%AE%AD%E5%A4%B4%E5%87%BD%E6%95%B0) - -  [3.6 对象](#user-content-36-%E5%AF%B9%E8%B1%A1) - -  [3.7 类](#user-content-37-%E7%B1%BB) - -  [3.8 模块](#user-content-38-%E6%A8%A1%E5%9D%97) - -  [3.9 集合](#user-content-39-%E9%9B%86%E5%90%88) - -  [3.10 异步](#user-content-310-%E5%BC%82%E6%AD%A5) - -[4 环境](#user-content-4-%E7%8E%AF%E5%A2%83) - -  [4.1 运行环境](#user-content-41-%E8%BF%90%E8%A1%8C%E7%8E%AF%E5%A2%83) - -  [4.2 预编译](#user-content-42-%E9%A2%84%E7%BC%96%E8%AF%91) - - - +# JavaScript 编码规范 - ESNext 补充篇 +**注:本文在公示完成后将合并至 JavaScript 编码规范。** ## 1 前言 - 随着 ECMAScript 的不断发展,越来越多更新的语言特性将被使用,给应用的开发带来方便。本文档的目标是使 ECMAScript 新特性的代码风格保持一致,并给予一些实践建议。 本文档仅包含新特性部分。基础部分请遵循 [JavaScript Style Guide](javascript-style-guide.md)。 @@ -61,79 +13,21 @@ 虽然本文档是针对 ECMAScript 设计的,但是在使用各种基于 ECMAScript 扩展的语言时(如 JSX、TypeScript 等),适用的部分也应尽量遵循本文档的约定。 - - - - ## 2 代码风格 - - - - ### 2.1 文件 - ##### [建议] ESNext 语法的 JavaScript 文件使用 `.js` 扩展名。 -##### [强制] 当文件无法使用 `.js` 扩展名时,使用 `.es` 扩展名。 - 解释: -某些应用开发时,可能同时包含 ES 5和 ESNext 文件,运行环境仅支持 ES5,ESNext 文件需要经过预编译。部分场景下,编译工具的选择可能需要通过扩展名区分,需要重新定义ESNext文件的扩展名。此时,ESNext 文件必须使用 `.es` 扩展名。 - -但是,更推荐使用其他条件作为是否需要编译的区分: - -1. 基于文件内容。 -2. 不同类型文件放在不同目录下。 - - - +所谓更新的 ES 标准,本身也是 ES 的一部分,保持 `.js` 扩展名即可。 +对于特定情况,使用 ES Module 的文件,可以有选择地使用 `.mjs` 后缀,对于 CommonJS 的文件,可以使用 `.cjs` 后缀。 ### 2.2 结构 - -#### 2.2.1 缩进 - - -##### [建议] 使用多行模板字符串时遵循缩进原则。当空行与空白字符敏感时,不使用多行模板字符串。 - -解释: - -`4` 空格为一个缩进,换行后添加一层缩进。将起始和结束的 `` ` `` 符号单独放一行,有助于生成 HTML 时的标签对齐。 - -为避免破坏缩进的统一,当空行与空白字符敏感时,建议使用 `多个模板字符串` 或 `普通字符串` 进行连接运算,也可使用数组 `join` 生成字符串。 - -示例: - -```javascript -// good -function foo() { - let html = ` -
-

-

-
- `; -} - -// Good -function greeting(name) { - return 'Hello, \n' - + `${name.firstName} ${name.lastName}`; -} - -// Bad -function greeting(name) { - return `Hello, -${name.firstName} ${name.lastName}`; -} -``` - - -#### 2.2.2 空格 - +#### 2.2.1 空格 ##### [强制] 使用 `generator` 时,`*` 前面不允许有空格,`*` 后面必须有一个空格。 @@ -155,8 +49,7 @@ function * caller() { } ``` - -#### 2.2.3 语句 +#### 2.2.2 语句 ##### [强制] 类声明结束不允许添加分号。 @@ -165,40 +58,11 @@ function * caller() { 与函数声明保持一致。 - -##### [强制] 类成员定义中,方法定义后不允许添加分号,成员属性定义后必须添加分号。 - -解释: - -成员属性是当前 **Stage 0** 的标准,如果使用的话,则定义后加上分号。 - -示例: - -```javascript -// good -class Foo { - foo = 3; - - bar() { - - } -} - -// bad -class Foo { - foo = 3 - - bar() { - - } -} -``` - ##### [强制] `export` 语句后,不允许出现表示空语句的分号。 解释: -`export` 关键字不影响后续语句类型。 +`export` 关键字不影响后续语句类型,如后缀是一个函数定义,则它依然是函数定义而不需要结尾的分号,类定义同理。 示例: @@ -219,46 +83,6 @@ export default function bar() { }; ``` - -##### [强制] 属性装饰器后,可以不加分号的场景,不允许加分号。 - -解释: - -只有一种场景是必须加分号的:当属性 `key` 是 `computed property key` 时,其装饰器必须加分号,否则修饰 `key` 的 `[]` 会做为之前表达式的 `property accessor`。 - -上面描述的场景,装饰器后需要加分号。其余场景下的属性装饰器后不允许加分号。 - -示例: - -```javascript -// good -class Foo { - @log('INFO') - bar() { - - } - - @log('INFO'); - ['bar' + 2]() { - - } -} - -// bad -class Foo { - @log('INFO'); - bar() { - - } - - @log('INFO') - ['bar' + 2]() { - - } -} -``` - - ##### [强制] 箭头函数的参数只有一个,并且不包含解构时,参数部分的括号必须省略。 示例: @@ -305,13 +129,18 @@ list.map(item => { }); ``` -##### [建议] 箭头函数的函数体只有一个 `Object Literal`,且作为返回值时,使用 `()` 包裹。 +##### [建议] 箭头函数的函数体只有一个 `Object Literal`,且作为返回值时,不使用 `return` ,使用 `()` 包裹返回的对象。 示例: ```javascript // good list.map(item => ({name: item[0], email: item[1]})); + +// bad +list.map(item => { + return {name: item[0], email: item[1]}; +}); ``` ##### [强制] 解构多个变量时,如果超过行长度限制,每个解构的变量必须单独一行。 @@ -337,16 +166,56 @@ let {name: personName, email: personEmail, } = person; ``` +##### [建议] 对于对象初始化、数组初始化、`import` 及 `export` 语句,当其多个子元素占用多行时,最后一个元素后必须保留逗号。 +解释: +ES Next允许以上语法最后保留逗号,多行时保留逗号有助于快速添加或删除子元素。 +本条规则不适用于函数调用和声明的参数,函数最后一个参数不得包含逗号。 +示例: -## 3 语言特性 +```javascript +// good +const array = [ + foo, + bar, +]; + +const array = [foo, bar]; // 单行的最后没有逗号 + +const object = { + foo: bar, + x: y, +}; + +const object = {foo: bar}; // 单行的最后没有逗号 +import { + foo, + bar, +} from 'module'; +import {foo, bar} from 'module'; // 单行的最后没有逗号 +export { + foo, + bar, +}; + +export {foo, bar}; // 单行的最后没有逗号 + +// bad +const array = [foo, bar,]; + +const object = { + foo: bar, + x: y +}; +``` +## 3 语言特性 ### 3.1 变量 @@ -371,12 +240,9 @@ for (var i = 0; i < 10; i++) { } ``` - - ### 3.2 解构 - -#### [强制] 不要使用3层及以上的解构。 +#### [建议] 不要使用3层及以上的解构。 解释: @@ -423,11 +289,11 @@ let len = myString.length; let {length: len} = myString; ``` -#### [强制] 如果不节省编写时产生的中间变量,解构表达式 `=` 号右边不允许是 `ObjectLiteral` 和 `ArrayLiteral`。 +#### [建议] 如果不节省编写时产生的中间变量,解构表达式 `=` 号右边不允许是 `ObjectLiteral` 和 `ArrayLiteral`。 解释: -在这种场景下,使用解构将降低代码可读性,通常也并无收益。 +在这种场景下,使用解构将降低代码可读性,通常也并无收益。如果2个变量有非常强的相关性,可以考虑使用 `ArrayLiteral` 进行声明,但通常我们并不会遇到此类场景。 示例: @@ -458,13 +324,9 @@ let other = myArray.slice(3); let [,,, ...other] = myArray; ``` - - ### 3.3 模板字符串 - - -#### [强制] 字符串内变量替换时,不要使用 `2` 次及以上的函数调用。 +#### [建议] 字符串内变量替换时,不要使用 `2` 次及以上的函数调用。 解释: @@ -481,17 +343,11 @@ let s = `Hello ${fullName}`; let s = `Hello ${getFullName(getFirstName(), getLastName())}`; ``` - - ### 3.4 函数 #### [建议] 使用变量默认语法代替基于条件判断的默认值声明。 -解释: - -添加默认值有助于引擎的优化,在未来 `strong mode` 下也会有更好的效果。 - 示例: ```javascript @@ -508,10 +364,6 @@ function foo(text) { #### [强制] 不要使用 `arguments` 对象,应使用 `...args` 代替。 -解释: - -在未来 `strong mode` 下 `arguments` 将被禁用。 - 示例: ```javascript @@ -526,21 +378,14 @@ function foo() { } ``` - - - ### 3.5 箭头函数 - - #### [强制] 一个函数被设计为需要 `call` 和 `apply` 的时候,不能是箭头函数。 解释: 箭头函数会强制绑定当前环境下的 `this`。 - - ### 3.6 对象 @@ -579,7 +424,9 @@ let foo2 = { 解释: -`MethodDefinition` 语法更清晰简洁。 +方法和函数是不同的东西,方法是与该对象相关的,通常会使用 `this` ,而函数则是更多是独立的逻辑,此时将对象视为一个类似命名空间的容器,本身函数与对象间没有强从属关系。 + +因此,当定义一个对象的方法时,应当明确使用 `MethodDefinition` 语法。 示例: @@ -619,29 +466,6 @@ for (let [key, value] of Object.entries(foo)) { } ``` -#### [建议] 定义对象的方法不应使用箭头函数。 - -解释: - -箭头函数将 `this` 绑定到当前环境,在 `obj.method()` 调用时容易导致不期待的 `this`。除非明确需要绑定 `this`,否则不应使用箭头函数。 - -示例: - -```javascript -// good -let foo = { - bar(x, y) { - return x + y; - } -}; - -// bad -let foo = { - bar: (x, y) => x + y -}; -``` - - #### [建议] 尽量使用计算属性键在一个完整的字面量中完整地定义一个对象,避免对象定义后直接增加对象属性。 解释: @@ -663,14 +487,8 @@ let foo = {}; foo[MY_KEY + 'Hash'] = 123; ``` - - - - ### 3.7 类 - - #### [强制] 使用 `class` 关键字定义一个类。 解释: @@ -735,12 +553,8 @@ class TextNode extends Node { } ``` - - ### 3.8 模块 - - #### [强制] `export` 与内容定义放在一起。 解释: @@ -756,7 +570,6 @@ export function foo() { export const bar = 3; - // bad function foo() { } @@ -775,8 +588,6 @@ export {bar}; 简而言之,当一个模块只扮演命名空间的作用时,使用命名导出。 - - #### [强制] 所有 `import` 语句写在模块开始处。 示例: @@ -796,12 +607,8 @@ function foo() { } ``` - - - ### 3.9 集合 - #### [建议] 对数组进行连接操作时,使用数组展开语法。 解释: @@ -820,20 +627,21 @@ let foo = foo.concat(newValue); let bar = bar.concat(newValues); ``` -#### [建议] 不要使用数组展开进行数组的复制操作。 +#### [建议] 使用数组展开语法进行数组的复制操作。 解释: -使用数组展开语法进行复制,代码可读性较差。推荐使用 `Array.from` 方法进行复制操作。 +使用数组展开可以快速地进行数组复制,也便于插入、追加其它元素。 示例: ```javascript // good -let otherArr = Array.from(arr); +let otherArr = [...arr]; // bad -let otherArr = [...arr]; +let otherArr = arr.slice(); +let otherArr = Array.from(arr); ``` #### [建议] 尽可能使用 `for .. of` 进行遍历。 @@ -847,6 +655,7 @@ let otherArr = [...arr]; 1. 遍历确实成为了性能瓶颈,需要使用原生 `for` 循环提升性能。 2. 需要遍历过程中的索引。 +当然,如果可以使用 `filter` 或 `map` 等函数完成逻辑,不建议使用 `for .. of` 的遍历来实现。 #### [强制] 当键值有可能不是字符串时,必须使用 `Map`;当元素有可能不是字符串时,必须使用 `Set`。 @@ -854,6 +663,7 @@ let otherArr = [...arr]; 使用普通 Object,对非字符串类型的 `key`,需要自己实现序列化。并且运行过程中的对象变化难以通知 Object。 +同时,如果一个对象从语义上应当是一个 `Map` 或 `Set` 对象,建议直接使用这2个类型,而不是使用 `Obejct` 来模拟行为。 #### [建议] 需要一个不可重复的集合时,应使用 `Set`。 @@ -875,7 +685,6 @@ let members = { }; ``` - #### [建议] 当需要遍历功能时,使用 `Map` 和 `Set`。 解释: @@ -931,9 +740,6 @@ membersAge.three = 30; delete membersAge['one']; ``` - - - ### 3.10 异步 @@ -991,7 +797,6 @@ getUser(userId) ); ``` - #### [强制] 使用标准的 `Promise` API。 解释: @@ -1001,7 +806,6 @@ getUser(userId) 使用标准的 `Promise` API,当运行环境都支持时,可以把 Promise Lib 直接去掉。 - #### [强制] 不允许直接扩展 `Promise` 对象的 `prototype`。 解释: @@ -1041,87 +845,27 @@ async function requestData() { } ``` -#### [建议] 使用 `async/await` 代替 `generator` + `co`。 - -解释: - -使用语言自身的能力可以使代码更清晰,也无需引入 `co` 库。 - -示例: - -```javascript -addReport(report, userId).then( - function () { - notice('Saved!'); - }, - function (message) { - notice(message); - } -); - -// good -async function addReport(report, userId) { - let user = await getUser(userId); - let isValid = await validateUser(user); - - if (isValid) { - let savePromise = saveReport(report, user); - return savePromise(); - } - - return Promise.reject('Invalid'); -} - -// bad -function addReport(report, userId) { - return co(function* () { - let user = yield getUser(userId); - let isValid = yield validateUser(user); - - if (isValid) { - let savePromise = saveReport(report, user); - return savePromise(); - } - - return Promise.reject('Invalid'); - }); -} -``` - - - - - - - - - ## 4 环境 - - - - - ### 4.1 运行环境 - #### [建议] 持续跟进与关注运行环境对语言特性的支持程度。 解释: -[查看环境对语言特性的支持程度](https://kangax.github.io/compat-table/es6/) +你可以通过 [MDN](https://developer.mozilla.org/zh-CN/) 或 [Can I use](https://caniuse.com/) 来查询各类特性在浏览器中的支持程度。 + +你可以通过 [Browserslist](https://browsersl.ist/) 来向各类工具提供你需要兼容的运行时,以便各类工具自动判断进行源码的转换。 ES 标准的制定还在不断进行中,各种环境对语言特性的支持也日新月异。了解项目中用到了哪些 ESNext 的特性,了解项目的运行环境,并持续跟进这些特性在运行环境中的支持程度是很有必要的。这意味着: -1. 如果有任何一个运行环境(比如 chrome)支持了项目里用到的所有特性,你可以在开发时抛弃预编译。 -2. 如果所有环境都支持了某一特性(比如 Promise),你可以抛弃相关的 shim,或无需在预编译时进行转换。 +1. 如果有任何一个运行环境(比如 Chrome)支持了项目里用到的所有特性,你可以在开发时抛弃预编译。 +2. 如果所有环境都支持了某一特性(比如 `Array.prototype.flatMap`),你可以抛弃相关的 shim,或无需在预编译时进行转换。 3. 如果所有环境都支持了项目里用到的所有特性,你可以完全抛弃预编译。 无论如何,在选择预编译工具时,你都需要清晰的知道你现阶段将在项目里使用哪些语言特性,然后了解预编译工具对语言特性的支持程度,做出选择。 - -#### [强制] 在运行环境中没有 `Promise` 时,将 `Promise` 的实现 `shim` 到 `global` 中。 +#### [强制] 在运行环境中没有 `Promise` 时,将 `Promise` 的实现 shim 到 `global` 中。 解释: @@ -1129,106 +873,4 @@ ES 标准的制定还在不断进行中,各种环境对语言特性的支持 这样,未来运行环境支持时,可以随时把 `Promise` 扩展直接扔掉,而应用代码无需任何修改。 - - - - -### 4.2 预编译 - - -#### [建议] 使用 `babel` 做为预编译工具时,建议使用 `5.x` 版本。 - -解释: - -由于 `babel` 最新的 `6` 暂时还不稳定,建议暂时使用 `5.x`。不同的产品,对于浏览器支持的情况不同,使用 `babel` 的时候,需要设置的参数也有一些区别。下面在示例中给出一些建议的参数。 - -示例: - -```shell -# 建议的参数 ---loose all --modules amd --blacklist strict - -# 如果需要使用 es7.classProperties、es7.decorators 等一些特性,需要额外的 --stage 0 参数 ---loose all --modules amd --blacklist strict --stage 0 -``` - - -#### [建议] 使用 `babel` 做为预编译工具时,通过 `external-helpers` 减少生成文件的大小。 - -解释: - -当 `babel` 在转换代码的过程中发现需要一些特性时,会在该文件头部生成对应的 `helper` 代码。默认情况下,对于每一个经由 `babel` 处理的文件,均会在文件头部生成对应需要的辅助函数,多份文件辅助函数存在重复,占用了不必要的代码体积。 - -因此推荐打开`externalHelpers: true`选项,使 `babel` 在转换后内容中不写入 `helper` 相关的代码,而是使用一个外部的 `.js`统一提供所有的 `helper`。对于[external-helpers](https://github.com/babel/babel.github.io/blob/5.0.0/docs/usage/external-helpers.md)的使用,可以有两种方式: - -1. 默认方式:需要通过 ` -```` - - ### 2.3 标签 - #### [强制] 标签名必须使用小写字母。 示例: @@ -196,7 +117,6 @@ alert(document.getElementById('foo').tagName); 常见无需自闭合标签有 `input`、`br`、`img`、`hr` 等。 - 示例: ```html @@ -213,7 +133,6 @@ alert(document.getElementById('foo').tagName); 对代码体积要求非常严苛的场景,可以例外。比如:第三方页面使用的投放系统。 - 示例: ```html @@ -230,15 +149,13 @@ alert(document.getElementById('foo').tagName); ``` - #### [强制] 标签使用必须符合标签嵌套规则。 解释: 比如 `div` 不得置于 `p` 中,`tbody` 必须置于 `table` 中。 -详细的标签嵌套规则参见[HTML DTD](http://www.cs.tut.fi/~jkorpela/html5.dtd)中的 `Elements` 定义部分。 - +详细的标签嵌套规则参见[HTMLf外](https://html.spec.whatwg.org/multipage/)中各元素定义的“Content model”说明。 #### [建议] HTML 标签的使用应该遵循标签的语义。 @@ -260,7 +177,6 @@ alert(document.getElementById('foo').tagName); - ol - 有序列表 - dl,dt,dd - 定义列表 - 示例: ```html @@ -271,14 +187,6 @@ alert(document.getElementById('foo').tagName);
Esprima serves as an important building block for some JavaScript language tools.
``` - -#### [建议] 在 CSS 可以实现相同需求的情况下不得使用表格进行布局。 - -解释: - -在兼容性允许的情况下应尽量保持语义正确性。对网格对齐和拉伸性有严格要求的场景允许例外,如多列复杂表单。 - - #### [建议] 标签的使用应尽量简洁,减少不必要的标签。 示例: @@ -293,11 +201,8 @@ alert(document.getElementById('foo').tagName); ``` - - ### 2.4 属性 - #### [强制] 属性名必须使用小写字母。 示例: @@ -310,14 +215,12 @@ alert(document.getElementById('foo').tagName); ...
``` - #### [强制] 属性值必须用双引号包围。 解释: 不允许使用单引号,不允许不使用引号。 - 示例: ```html @@ -338,29 +241,23 @@ alert(document.getElementById('foo').tagName); ``` - #### [建议] 自定义属性建议以 `xxx-` 为前缀,推荐使用 `data-`。 解释: 使用前缀有助于区分自定义属性和标准定义的属性。 - 示例: ```html
    ``` - - ## 3 通用 - ### 3.1 DOCTYPE - -#### [强制] 使用 `HTML5` 的 `doctype` 来启用标准模式,建议使用大写的 `DOCTYPE`。 +#### [强制] 使用 `HTML5` 的 `doctype` 来启用标准模式,使用大写的 `DOCTYPE`。 示例: @@ -368,31 +265,20 @@ alert(document.getElementById('foo').tagName); ``` -#### [建议] 启用 IE Edge 模式。 - -示例: - -```html - -``` - #### [建议] 在 `html` 标签上设置正确的 `lang` 属性。 解释: 有助于提高页面的可访问性,如:让语音合成工具确定其所应该采用的发音,令翻译工具确定其翻译语言等。 - 示例: ```html ``` - ### 3.2 编码 - #### [强制] 页面必须使用精简形式,明确指定字符编码。指定字符编码的 `meta` 必须是 `head` 的第一个直接子元素。 解释: @@ -413,17 +299,8 @@ alert(document.getElementById('foo').tagName); ``` -#### [建议] `HTML` 文件使用无 `BOM` 的 `UTF-8` 编码。 - -解释: - -`UTF-8` 编码具有更广泛的适应性。`BOM` 在使用程序或工具处理文件时可能造成不必要的干扰。 - - - ### 3.3 CSS 和 JavaScript 引入 - #### [强制] 引入 `CSS` 时必须指明 `rel="stylesheet"`。 示例: @@ -432,20 +309,13 @@ alert(document.getElementById('foo').tagName); ``` - -#### [建议] 引入 `CSS` 和 `JavaScript` 时无须指明 `type` 属性。 +#### [建议] 引入 `CSS` 和脚本类 `JavaScript` 时无须指明 `type` 属性。 解释: `text/css` 和 `text/javascript` 是 `type` 的默认值。 - -#### [建议] 展现定义放置于外部 `CSS` 中,行为定义放置于外部 `JavaScript` 中。 - -解释: - -结构-样式-行为的代码分离,对于提高代码的可阅读性和维护性都有好处。 - +特别的,当以模块的形式引入 JavaScript 时,需要 `type="module"` 属性。 #### [建议] 在 `head` 中引入页面需要的所有 `CSS` 资源。 @@ -454,28 +324,30 @@ alert(document.getElementById('foo').tagName); 在页面渲染的过程中,新的CSS可能导致元素的样式重新计算和绘制,页面闪烁。 -#### [建议] `JavaScript` 应当放在页面末尾,或采用异步加载。 +#### [建议] `JavaScript` 应当放在页面末尾,或采用延迟、异步加载。 解释: 将 `script` 放在页面中间将阻断页面的渲染。出于性能方面的考虑,如非必要,请遵守此条建议。 +也可以使用 `async` 或 `defer` 属性来延迟脚本的执行。 示例: ```html + + ``` - #### [建议] 移动环境或只针对现代浏览器设计的 Web 应用,如果引用外部资源的 `URL` 协议部分与页面相同,建议省略协议前缀。 解释: -使用 `protocol-relative URL` 引入 CSS,在 `IE7/8` 下,会发两次请求。是否使用 `protocol-relative URL` 应充分考虑页面针对的环境。 +省略协议可很好地兼容 HTTP 与 HTTPS 等多种环境。 示例: @@ -484,17 +356,10 @@ alert(document.getElementById('foo').tagName); ``` - - - - - ## 4 head - ### 4.1 title - #### [强制] 页面必须包含 `title` 标签声明标题。 #### [强制] `title` 必须作为 `head` 的直接子元素,并紧随 `charset` 声明之后。 @@ -503,7 +368,6 @@ alert(document.getElementById('foo').tagName); `title` 中如果包含 ASCII 之外的字符,浏览器需要知道字符编码类型才能进行解码,否则可能导致乱码。 - 示例: ```html @@ -515,7 +379,6 @@ alert(document.getElementById('foo').tagName); ### 4.2 favicon - #### [强制] 保证 `favicon` 可访问。 解释: @@ -525,7 +388,6 @@ alert(document.getElementById('foo').tagName); 1. 在 Web Server 根目录放置 `favicon.ico` 文件。 2. 使用 `link` 指定 favicon。 - 示例: ```html @@ -534,31 +396,24 @@ alert(document.getElementById('foo').tagName); ### 4.3 viewport - #### [建议] 若页面欲对移动设备友好,需指定页面的 `viewport`。 解释: -viewport meta tag 可以设置可视区域的宽度和初始缩放大小,避免在移动设备上出现页面展示不正常。 +`viewport` meta tag 可以设置可视区域的宽度和初始缩放大小,避免在移动设备上出现页面展示不正常。 比如,在页面宽度小于 `980px` 时,若需 iOS 设备友好,应当设置 viewport 的 `width` 值来适应你的页面宽度。同时因为不同移动设备分辨率不同,在设置时,应当使用 `device-width` 和 `device-height` 变量。 另外,为了使 viewport 正常工作,在页面内容样式布局设计上也要做相应调整,如避免绝对定位等。关于 viewport 的更多介绍,可以参见 [Safari Web Content Guide的介绍](https://developer.apple.com/library/mac/documentation/AppleApplications/Reference/SafariWebContent/UsingtheViewport/UsingtheViewport.html#//apple_ref/doc/uid/TP40006509-SW26) - - - ## 5 图片 - - #### [强制] 禁止 `img` 的 `src` 取值为空。延迟加载的图片也要增加默认的 `src`。 解释: `src` 取值为空,会导致部分浏览器重新加载一次当前页面,参考: - #### [建议] 避免为 `img` 添加不必要的 `title` 属性。 解释: @@ -569,25 +424,14 @@ viewport meta tag 可以设置可视区域的宽度和初始缩放大小,避 解释: -可以提高图片加载失败时的用户体验。 +可以提高图片加载失败时的用户体验和可访问性。 #### [建议] 添加 `width` 和 `height` 属性,以避免页面抖动。 -#### [建议] 有下载需求的图片采用 `img` 标签实现,无下载需求的图片采用 CSS 背景图实现。 - -解释: - -1. 产品 logo、用户头像、用户产生的图片等有潜在下载需求的图片,以 `img` 形式实现,能方便用户下载。 -2. 无下载需求的图片,比如:icon、背景、代码使用的图片等,尽可能采用 CSS 背景图实现。 - - - ## 6 表单 - ### 6.1 控件标题 - #### [强制] 有文本标题的控件必须使用 `label` 标签将其与其标题相关联。 解释: @@ -599,7 +443,6 @@ viewport meta tag 可以设置可视区域的宽度和初始缩放大小,避 推荐使用第一种,减少不必要的 `id`。如果 DOM 结构不允许直接嵌套,则应使用第二种。 - 示例: ```html @@ -607,18 +450,14 @@ viewport meta tag 可以设置可视区域的宽度和初始缩放大小,避 ``` - - ### 6.2 按钮 - #### [强制] 使用 `button` 元素时必须指明 `type` 属性值。 解释: `button` 元素的默认 `type` 为 `submit`,如果被置于 `form` 元素中,点击后将导致表单提交。为显示区分其作用方便理解,必须给出 `type` 属性。 - 示例: ```html @@ -626,90 +465,8 @@ viewport meta tag 可以设置可视区域的宽度和初始缩放大小,避 ``` -#### [建议] 尽量不要使用按钮类元素的 `name` 属性。 - -解释: - -由于浏览器兼容性问题,使用按钮的 `name` 属性会带来许多难以发现的问题。具体情况可参考[此文](http://w3help.org/zh-cn/causes/CM2001)。 - - -### 6.3 可访问性 (A11Y) - - -#### [建议] 负责主要功能的按钮在 DOM 中的顺序应靠前。 - -解释: - -负责主要功能的按钮应相对靠前,以提高可访问性。如果在 CSS 中指定了 `float: right` 则可能导致视觉上主按钮在前,而 DOM 中主按钮靠后的情况。 - - -示例: - -```html - - - -
    -
    - - -
    -
    - - - - -
    - - -
    -``` - -#### [建议] 当使用 JavaScript 进行表单提交时,如果条件允许,应使原生提交功能正常工作。 - -解释: - -当浏览器 JS 运行错误或关闭 JS 时,提交功能将无法工作。如果正确指定了 `form` 元素的 `action` 属性和表单控件的 `name` 属性时,提交仍可继续进行。 - - -示例: - -```html -
    -

    -

    -
    -``` - -#### [建议] 在针对移动设备开发的页面时,根据内容类型指定输入框的 `type` 属性。 - -解释: - -根据内容类型指定输入框类型,能获得能友好的输入体验。 - - -示例: - -```html - -``` - - - - - ## 7 多媒体 - - #### [建议] 当在现代浏览器中使用 `audio` 以及 `video` 标签来播放音频、视频时,应当注意格式。 解释: @@ -728,31 +485,8 @@ viewport meta tag 可以设置可视区域的宽度和初始缩放大小,避 #### [建议] 在支持 `HTML5` 的浏览器中优先使用 `audio` 和 `video` 标签来定义音视频元素。 -#### [建议] 使用退化到插件的方式来对多浏览器进行支持。 - -示例: - -```html - - - -``` - #### [建议] 只在必要的时候开启音视频的自动播放。 - #### [建议] 在 `object` 标签内部提供指示浏览器不支持该标签的说明。 示例: @@ -761,12 +495,8 @@ viewport meta tag 可以设置可视区域的宽度和初始缩放大小,避 DO NOT SUPPORT THIS TAG ``` - - - ## 8 模板中的 HTML - #### [建议] 模板代码的缩进优先保证 HTML 代码的缩进规则。 示例: @@ -837,6 +567,18 @@ viewport meta tag 可以设置可视区域的宽度和初始缩放大小,避 ``` +#### [强制] 新窗口/标签页打开的链接需要按业务需要添加安全相关的 `rel` 属性值 `noopener`。 + +解释: + +新窗口、标签页打开的网页可以通过 `window.opener` 访问原网页的上下文,会引入安全问题。例如从搜索结果页打开的网页可以通过 `window.opener` 将结果页篡改为钓鱼网页,用户返回时将访问恶意网页内容。 +示例: +```html + +外部链接 + +外部链接 +``` diff --git a/javascript-style-guide.md b/javascript-style-guide.md index 08fae91..311ce8b 100644 --- a/javascript-style-guide.md +++ b/javascript-style-guide.md @@ -1,168 +1,41 @@ # JavaScript编码规范 - - - -[1 前言](#user-content-1-%E5%89%8D%E8%A8%80) - -[2 代码风格](#user-content-2-%E4%BB%A3%E7%A0%81%E9%A3%8E%E6%A0%BC) - -  [2.1 文件](#user-content-21-%E6%96%87%E4%BB%B6) - -  [2.2 结构](#user-content-22-%E7%BB%93%E6%9E%84) - -    [2.2.1 缩进](#user-content-221-%E7%BC%A9%E8%BF%9B) - -    [2.2.2 空格](#user-content-222-%E7%A9%BA%E6%A0%BC) - -    [2.2.3 换行](#user-content-223-%E6%8D%A2%E8%A1%8C) - -    [2.2.4 语句](#user-content-224-%E8%AF%AD%E5%8F%A5) - -  [2.3 命名](#user-content-23-%E5%91%BD%E5%90%8D) - -  [2.4 注释](#user-content-24-%E6%B3%A8%E9%87%8A) - -    [2.4.1 单行注释](#user-content-241-%E5%8D%95%E8%A1%8C%E6%B3%A8%E9%87%8A) - -    [2.4.2 多行注释](#user-content-242-%E5%A4%9A%E8%A1%8C%E6%B3%A8%E9%87%8A) - -    [2.4.3 文档化注释](#user-content-243-%E6%96%87%E6%A1%A3%E5%8C%96%E6%B3%A8%E9%87%8A) - -    [2.4.4 类型定义](#user-content-244-%E7%B1%BB%E5%9E%8B%E5%AE%9A%E4%B9%89) - -    [2.4.5 文件注释](#user-content-245-%E6%96%87%E4%BB%B6%E6%B3%A8%E9%87%8A) - -    [2.4.6 命名空间注释](#user-content-246-%E5%91%BD%E5%90%8D%E7%A9%BA%E9%97%B4%E6%B3%A8%E9%87%8A) - -    [2.4.7 类注释](#user-content-247-%E7%B1%BB%E6%B3%A8%E9%87%8A) - -    [2.4.8 函数/方法注释](#user-content-248-%E5%87%BD%E6%95%B0/%E6%96%B9%E6%B3%95%E6%B3%A8%E9%87%8A) - -    [2.4.9 事件注释](#user-content-249-%E4%BA%8B%E4%BB%B6%E6%B3%A8%E9%87%8A) - -    [2.4.10 常量注释](#user-content-2410-%E5%B8%B8%E9%87%8F%E6%B3%A8%E9%87%8A) - -    [2.4.11 复杂类型注释](#user-content-2411-%E5%A4%8D%E6%9D%82%E7%B1%BB%E5%9E%8B%E6%B3%A8%E9%87%8A) - -    [2.4.12 AMD 模块注释](#user-content-2412-amd-%E6%A8%A1%E5%9D%97%E6%B3%A8%E9%87%8A) - -    [2.4.13 细节注释](#user-content-2413-%E7%BB%86%E8%8A%82%E6%B3%A8%E9%87%8A) - -[3 语言特性](#user-content-3-%E8%AF%AD%E8%A8%80%E7%89%B9%E6%80%A7) - -  [3.1 变量](#user-content-31-%E5%8F%98%E9%87%8F) - -  [3.2 条件](#user-content-32-%E6%9D%A1%E4%BB%B6) - -  [3.3 循环](#user-content-33-%E5%BE%AA%E7%8E%AF) - -  [3.4 类型](#user-content-34-%E7%B1%BB%E5%9E%8B) - -    [3.4.1 类型检测](#user-content-341-%E7%B1%BB%E5%9E%8B%E6%A3%80%E6%B5%8B) - -    [3.4.2 类型转换](#user-content-342-%E7%B1%BB%E5%9E%8B%E8%BD%AC%E6%8D%A2) - -  [3.5 字符串](#user-content-35-%E5%AD%97%E7%AC%A6%E4%B8%B2) - -  [3.6 对象](#user-content-36-%E5%AF%B9%E8%B1%A1) - -  [3.7 数组](#user-content-37-%E6%95%B0%E7%BB%84) - -  [3.8 函数](#user-content-38-%E5%87%BD%E6%95%B0) - -    [3.8.1 函数长度](#user-content-381-%E5%87%BD%E6%95%B0%E9%95%BF%E5%BA%A6) - -    [3.8.2 参数设计](#user-content-382-%E5%8F%82%E6%95%B0%E8%AE%BE%E8%AE%A1) - -    [3.8.3 闭包](#user-content-383-%E9%97%AD%E5%8C%85) - -    [3.8.4 空函数](#user-content-384-%E7%A9%BA%E5%87%BD%E6%95%B0) - -  [3.9 面向对象](#user-content-39-%E9%9D%A2%E5%90%91%E5%AF%B9%E8%B1%A1) - -  [3.10 动态特性](#user-content-310-%E5%8A%A8%E6%80%81%E7%89%B9%E6%80%A7) - -    [3.10.1 eval](#user-content-3101-eval) - -    [3.10.2 动态执行代码](#user-content-3102-%E5%8A%A8%E6%80%81%E6%89%A7%E8%A1%8C%E4%BB%A3%E7%A0%81) - -    [3.10.3 with](#user-content-3103-with) - -    [3.10.4 delete](#user-content-3104-delete) - -    [3.10.5 对象属性](#user-content-3105-%E5%AF%B9%E8%B1%A1%E5%B1%9E%E6%80%A7) - -[4 浏览器环境](#user-content-4-%E6%B5%8F%E8%A7%88%E5%99%A8%E7%8E%AF%E5%A2%83) - -  [4.1 模块化](#user-content-41-%E6%A8%A1%E5%9D%97%E5%8C%96) - -    [4.1.1 AMD](#user-content-411-amd) - -    [4.1.2 define](#user-content-412-define) - -    [4.1.3 require](#user-content-413-require) - -  [4.2 DOM](#user-content-42-dom) - -    [4.2.1 元素获取](#user-content-421-%E5%85%83%E7%B4%A0%E8%8E%B7%E5%8F%96) - -    [4.2.2 样式获取](#user-content-422-%E6%A0%B7%E5%BC%8F%E8%8E%B7%E5%8F%96) - -    [4.2.3 样式设置](#user-content-423-%E6%A0%B7%E5%BC%8F%E8%AE%BE%E7%BD%AE) - -    [4.2.4 DOM 操作](#user-content-424-dom-%E6%93%8D%E4%BD%9C) - -    [4.2.5 DOM 事件](#user-content-425-dom-%E4%BA%8B%E4%BB%B6) - - - - - ## 1 前言 - JavaScript 在百度一直有着广泛的应用,特别是在浏览器端的行为管理。本文档的目标是使 JavaScript 代码风格保持一致,容易被理解和被维护。 虽然本文档是针对 JavaScript 设计的,但是在使用各种 JavaScript 的预编译语言时(如 TypeScript 等)时,适用的部分也应尽量遵循本文档的约定。 - - - - ## 2 代码风格 - - - - - ### 2.1 文件 - -##### [建议] JavaScript 文件使用无 `BOM` 的 `UTF-8` 编码。 +##### [建议] 在文件结尾处,保留一个空行。 解释: -UTF-8 编码具有更广泛的适应性。BOM 在使用程序或工具处理文件时可能造成不必要的干扰。 +文件结尾保留空行有利于各类工具对文件进行处理,如命令行的 `cat` 命令等。 -##### [建议] 在文件结尾处,保留一个空行。 +你可以使用 [EditorConfig](https://editorconfig.org/) 中的 `insert_final_newline = true` 配置自动化处理本条规则。 +##### [建议] 使用LF作为换行符。 +解释: +全局使用统一的换行符有助于自动化工具对文本进行处理,绝大部分社区工具依赖于换行符为 `LF` 进行工作。 -### 2.2 结构 +你可以使用以下方式设置换行符: +- 使用 [EditorConfig](https://editorconfig.org/) 的 `end_of_line = lf` 配置。 +- 使用 `git config --global core.autocrlf true` 确保代码提交与下载保持一致性。 +### 2.2 结构 #### 2.2.1 缩进 - ##### [强制] 使用 `4` 个空格做为一个缩进层级,不允许使用 `2` 个空格 或 `tab` 字符。 - - ##### [强制] `switch` 下的 `case` 和 `default` 必须增加一个缩进层级。 示例: @@ -170,7 +43,6 @@ UTF-8 编码具有更广泛的适应性。BOM 在使用程序或工具处理文 ```javascript // good switch (variable) { - case '1': // do... break; @@ -181,12 +53,10 @@ switch (variable) { default: // do... - } // bad switch (variable) { - case '1': // do... break; @@ -197,20 +67,42 @@ case '2': default: // do... - } ``` -#### 2.2.2 空格 +##### [建议]当 `case` 下有局部变量声明时,在 `case` 层级增加大括号。 + +示例: + +```javascript +// good +switch (variable) { + case '1': { + let result = `Hello ${variable}`; + doSomeWork(result); + break; + } + // ... +} +// bad +switch (variable) { + case '1': + let result = `Hello ${variable}`; + doSomeWork(result); + break; + // ... +} +``` +#### 2.2.2 空格 ##### [强制] 二元运算符两侧必须有一个空格,一元运算符与操作对象之间不允许有空格。 示例: ```javascript -var a = !arr.length; +let a = !arr.length; a++; a = b + c; ``` @@ -273,14 +165,14 @@ while(condition) { ```javascript // good -var obj = { +let obj = { a: 1, b: 2, c: 3 }; // bad -var obj = { +let obj = { a : 1, b:2, c :3 @@ -296,7 +188,7 @@ var obj = { function funcName() { } -var funcName = function funcName() { +let funcName = function funcName() { }; funcName(); @@ -305,7 +197,7 @@ funcName(); function funcName () { } -var funcName = function funcName () { +let funcName = function funcName () { }; funcName (); @@ -369,38 +261,51 @@ while ( len-- ) { ```javascript // good -var arr1 = []; -var arr2 = [1, 2, 3]; -var obj1 = {}; -var obj2 = {name: 'obj'}; -var obj3 = { +let arr1 = []; +let arr2 = [1, 2, 3]; +let obj1 = {}; +let obj2 = {name: 'obj'}; +let obj3 = { name: 'obj', age: 20, sex: 1 }; // bad -var arr1 = [ ]; -var arr2 = [ 1, 2, 3 ]; -var obj1 = { }; -var obj2 = { name: 'obj' }; -var obj3 = {name: 'obj', age: 20, sex: 1}; +let arr1 = [ ]; +let arr2 = [ 1, 2, 3 ]; +let obj1 = { }; +let obj2 = { name: 'obj' }; +let obj3 = {name: 'obj', age: 20, sex: 1}; ``` ##### [强制] 行尾不得有多余的空格。 +解释: + +除模板字符串外,一行代码的尾部不得有多余的空格。 + +跨行的模板字符串中允许有行尾空格,但出于格式化工具的行为一致性的考虑,仍不建设这样操作,如需要空格,建议在模板字符串中用变量占位声明。 + +```javascript +let message = ` +Hello, ${' '} +Goodbye. +`; +``` #### 2.2.3 换行 ##### [强制] 每个独立语句结束后必须换行。 -##### [强制] 每行不得超过 `120` 个字符。 +##### [强制] 每行不得超过 **120** 个字符。 解释: 超长的不可分割的代码允许例外,比如复杂的正则表达式。长字符串不在例外之列。 +对于长字符串,如果具备可分割性,且字符串需要被开发者阅读以了解其内容,则建议对字符串进行分隔,确保每行字符数小于120。 ##### [强制] 运算符处换行时,运算符必须在新行的行首。 @@ -416,7 +321,7 @@ if (user.isAuthenticated() // Code } -var result = number1 + number2 + number3 +let result = number1 + number2 + number3 + number4 + number5; @@ -428,7 +333,7 @@ if (user.isAuthenticated() && // Code } -var result = number1 + number2 + number3 + +let result = number1 + number2 + number3 + number4 + number5; ``` @@ -438,7 +343,7 @@ var result = number1 + number2 + number3 + ```javascript // good -var obj = { +let obj = { a: 1, b: 2, c: 3 @@ -452,7 +357,7 @@ foo( // bad -var obj = { +let obj = { a: 1 , b: 2 , c: 3 @@ -498,7 +403,7 @@ if (user.isAuthenticated() // 按一定长度截断字符串,并使用 + 运算符进行连接。 // 分隔字符串尽量按语义进行,如不要在一个完整的名词中间断开。 // 特别的,对于 HTML 片段的拼接,通过缩进,保持和 HTML 相同的结构。 -var html = '' // 此处用一个空字符串,以便整个 HTML 片段都在新行严格对齐 +let html = '' // 此处用一个空字符串,以便整个 HTML 片段都在新行严格对齐 + '
    ' + '

    Title here

    ' + '

    This is a paragraph

    ' @@ -506,15 +411,24 @@ var html = '' // 此处用一个空字符串,以便整个 HTML 片段都在新 + '
    '; // 也可使用数组来进行拼接,相对 `+` 更容易调整缩进。 -var html = [ +let html = [ '
    ', '

    Title here

    ', '

    This is a paragraph

    ', '
    Complete
    ', - '
    ' + '', ]; html = html.join(''); +// 你也可以用 `dedent` 这个库和模板字符串来实现 +let html = dedent` +
    +

    Title here

    +

    This is a paragraph

    +
    Complete
    +
    +`; + // 当参数过多时,将每个参数独立写在一行上,并将结束的右括号 ) 独立一行。 // 所有参数必须增加一个缩进。 foo( @@ -555,15 +469,15 @@ $('#items') .end(); // 三元运算符由3部分组成,因此其换行应当根据每个部分的长度不同,形成不同的情况。 -var result = thisIsAVeryVeryLongCondition +let result = thisIsAVeryVeryLongCondition ? resultA : resultB; -var result = condition +let result = condition ? thisIsAVeryVeryLongResult : resultB; // 数组和对象初始化的混用,严格按照每个对象的 `{` 和结束 `}` 在独立一行的风格书写。 -var array = [ +let array = [ { // ... }, @@ -573,32 +487,52 @@ var array = [ ]; ``` -##### [建议] 对于 `if...else...`、`try...catch...finally` 等语句,推荐使用在 `}` 号后添加一个换行 的风格,使代码层次结构更清晰,阅读性更好。 +#### 2.2.4 语句 + + +##### [强制] 不得省略语句结束的分号。 + +对于函数声明(Function Declaration)、类声明(Class Declaration),它们不属于语句(Statement),因此不需要以分号结束。 + +对于 `export default` 语句,参考其后部分的类型,如果是函数、类声明,同样不需要分号。 示例: ```javascript -if (condition) { - // some statements; -} -else { - // some statements; +// good +function foo() { + // ... } -try { - // some statements; -} -catch (ex) { - // some statements; +class Foo { + // ... } -``` +let foo = function () { + // ... +}; // 这里需要有分号 +export default function foo() { + // ... +} // 这里不需要分号 -#### 2.2.4 语句 +// bad +function foo() { + // ... +}; +class Foo { + // ... +}; -##### [强制] 不得省略语句结束的分号。 +let foo = function () { + // ... +} + +export default function foo() { + // ... +}; +``` ##### [强制] 在 `if / else / for / do / while` 语句中,即使只有一行,也不得省略块 `{...}`。 @@ -630,7 +564,7 @@ function funcName() { }; // 如果是函数表达式,分号是不允许省略的。 -var funcName = function () { +let funcName = function () { }; ``` @@ -647,22 +581,22 @@ IIFE = Immediately-Invoked Function Expression. ```javascript // good -var task = (function () { +let task = (function () { // Code return result; })(); -var func = function () { +let func = function () { }; // bad -var task = function () { +let task = function () { // Code return result; }(); -var func = (function () { +let func = (function () { }); ``` @@ -673,87 +607,98 @@ var func = (function () { ### 2.3 命名 -##### [强制] `变量` 使用 `Camel命名法`。 +##### [强制] **变量**使用 `camelCase` 命名法。 示例: ```javascript -var loadingModules = {}; +let loadingModules = {}; ``` -##### [强制] `常量` 使用 `全部字母大写,单词间下划线分隔` 的命名方式。 +##### [强制] **常量**使用 `const` 关键字声明。 + +解释: + +此处常量特指**引用不会改变、内容也不会改变**的不变量。 示例: ```javascript -var HTML_ENTITY = {}; +const LOCAL_IP = '127.0.0.1'; ``` -##### [强制] `函数` 使用 `Camel命名法`。 +##### [强制] **常量**使用 `CONST_CASE` 命名法。 示例: ```javascript -function stringFormat(source) { -} +const HTML_ENTITY = {}; ``` -##### [强制] 函数的 `参数` 使用 `Camel命名法`。 +##### [强制] **函数**使用 `camelCase` 命名法。 示例: ```javascript -function hear(theBells) { +function stringFormat(source) { } ``` - -##### [强制] `类` 使用 `Pascal命名法`。 +##### [强制] 函数的**参数**使用 `camelCase` 命名法。 示例: ```javascript -function TextNode(options) { +function hear(theBells) { } ``` -##### [强制] 类的 `方法` / `属性` 使用 `Camel命名法`。 + +##### [强制] **类**使用 `PascalCase` 命名法。 示例: ```javascript -function TextNode(value, engine) { - this.value = value; - this.engine = engine; +class TextNode { } - -TextNode.prototype.clone = function () { - return this; -}; ``` -##### [强制] `枚举变量` 使用 `Pascal命名法`,`枚举的属性` 使用 `全部字母大写,单词间下划线分隔` 的命名方式。 +##### [强制] 类的**方法**和**属性**使用 `camelCase` 命名法 示例: ```javascript -var TargetState = { - READING: 1, - READED: 2, - APPLIED: 3, - READY: 4 -}; +class TextNode { + value = ''; + engine = null; + + constructor(value, engine) { + this.value = value; + this.engine = engine; + } + + clone() { + return this; + } +} ``` -##### [强制] `命名空间` 使用 `Camel命名法`。 +##### [强制] **枚举变量**使用 `PascalCase`,**枚举的属性**使用 `CONST_CASE` 命名法。 + +此条指 JavaScript 下的枚举模块,TypeScript 中的 `enum` 关键字参考 TypeScript 规范相关条目。 示例: ```javascript -equipments.heavyWeapons = {}; +const Permission = { + READ: 1, + READ_WRITE: 2, +}; ``` -##### [强制] 由多个单词组成的缩写词,在命名中,根据当前命名法和出现的位置,所有字母的大小写与首字母的大小写保持一致。 +##### [建议] 由多个单词组成的缩写词,在命名中,根据当前命名法和出现的位置,所有字母的大小写与首字母的大小写保持一致。 + +如果对缩写词有自己的命名方案,需要在项目内保持一致。 示例: @@ -764,25 +709,31 @@ function XMLParser() { function insertHTML(element, html) { } -var httpRequest = new HTTPRequest(); +let httpRequest = new HTTPRequest(); ``` -##### [强制] `类名` 使用 `名词`。 +##### [强制] **类名**使用 `名词`。 示例: ```javascript -function Engine(options) { +class Engine { } ``` -##### [建议] `函数名` 使用 `动宾短语`。 +##### [建议] **函数名**、**方法名**使用 `动宾短语`。 示例: ```javascript function getStyle(element) { } + +class Engine { + formatText(text) { + // ... + } +} ``` ##### [建议] `boolean` 类型的变量使用 `is` 或 `has` 开头。 @@ -790,63 +741,117 @@ function getStyle(element) { 示例: ```javascript -var isReady = false; -var hasMoreCommands = false; +let isReady = false; +let hasMoreCommands = false; ``` -##### [建议] `Promise对象` 用 `动宾短语的进行时` 表达。 +##### [建议] `Promise` 对象用 `动宾短语的进行时` 表达。 示例: ```javascript -var loadingData = ajax.get('url'); +let loadingData = ajax.get('url'); loadingData.then(callback); ``` - - - ### 2.4 注释 #### 2.4.1 单行注释 +##### [强制] 单行的注释以 `//` 起始,后跟一个空格,独占一行的情况下缩进与下一行被注释说明的代码一致。 -##### [强制] 必须独占一行。`//` 后跟一个空格,缩进与下一行被注释说明的代码一致。 +示例: -#### 2.4.2 多行注释 +```javascript +function processUserInput(input) { + // 对字符串进行转义后才能正常解析 + let escaped = escapeString(input); + outputToUser(escaped); // 用户看到的就是转义后的,没问题 +} +``` +#### 2.4.2 多行注释 ##### [建议] 避免使用 `/*...*/` 这样的多行注释。有多行注释内容时,使用多个单行注释。 +示例: + +```javascript +// good +// 这是一段复杂的逻辑: +// 1. ... +// 2. ... +function work() { + // ... +} + +// bad +/* + * 这是一段复杂的逻辑: + * 1. ... + * 2. ... + */ +function work() { + // ... +} +``` + +#### 2.4.3 待办注释 + +##### [建议] 待办类注释结构包含类型关键字、说明、负责人。 -#### 2.4.3 文档化注释 +格式为 `// 类型: 信息 @待办人`,注意类型后有一个分号,待办人前用 `@` 标识。 +```javascript +// TODO: 这里还缺一个分支没处理 @zhangsan +``` + +##### [建议] 使用统一、固定的待办类关键字。 + +建议使用如下关键字: + +- TODO:有功能待实现。此时需要对将要实现的功能进行简单说明。 +- NOTE:提示阅读者着重阅读。 +- WARN:警告此处代码非常重要。 +- HACK:为修正某些问题而写的不太好或者使用了某些诡异手段的代码。此时需要对思路或诡异手段进行描述。 +- FIXME:该处代码运行没问题,但可能由于时间赶或者其他原因,需要修正。此时需要对如何修正进行简单说明。 +- DEPRECATED:该部分代码已经过期作废。 + +你可以使用如 [Better Comments 插件](https://marketplace.visualstudio.com/items?itemName=aaron-bond.better-comments)等工具高亮待办类的注释,以取得最佳的代码阅读体验。 + +#### 2.4.4 文档化注释 ##### [强制] 为了便于代码阅读和自文档化,以下内容必须包含以 `/**...*/` 形式的块注释中。 解释: 1. 文件 -2. namespace 3. 类 4. 函数或方法 5. 类属性 6. 事件 7. 全局变量 8. 常量 -9. AMD 模块 - ##### [强制] 文档注释前必须空一行。 +```javascript +function foo() { +} -##### [建议] 自文档化的文档说明 what,而不是 how。 - +/** + * 这里是下一个函数的注释,前面要空一行 + */ +function bar() { +} +``` +##### [建议] 自文档化的文档说明 what,而不是 how。 -#### 2.4.4 类型定义 +#### 2.4.5 类型定义 +**类型定义类的注释均不涉及 TypeScript 编写的代码。** ##### [强制] 类型定义都是以 `{` 开始, 以 `}` 结束。 @@ -882,61 +887,7 @@ loadingData.then(callback); |可选任意类型|@param {*=} name|可选参数,类型不限| |可变任意类型|@param {...*} args|变长参数,类型不限| - -#### 2.4.5 文件注释 - - -##### [强制] 文件顶部必须包含文件注释,用 `@file` 标识文件说明。 - -示例: - -```javascript -/** - * @file Describe the file - */ -``` - -##### [建议] 文件注释中可以用 `@author` 标识开发者信息。 - -解释: - -开发者信息能够体现开发人员对文件的贡献,并且能够让遇到问题或希望了解相关信息的人找到维护人。通常情况文件在被创建时标识的是创建者。随着项目的进展,越来越多的人加入,参与这个文件的开发,新的作者应该被加入 `@author` 标识。 - -`@author` 标识具有多人时,原则是按照 `责任` 进行排序。通常的说就是如果有问题,就是找第一个人应该比找第二个人有效。比如文件的创建者由于各种原因,模块移交给了其他人或其他团队,后来因为新增需求,其他人在新增代码时,添加 `@author` 标识应该把自己的名字添加在创建人的前面。 - -`@author` 中的名字不允许被删除。任何劳动成果都应该被尊重。 - -业务项目中,一个文件可能被多人频繁修改,并且每个人的维护时间都可能不会很长,不建议为文件增加 `@author` 标识。通过版本控制系统追踪变更,按业务逻辑单元确定模块的维护责任人,通过文档与wiki跟踪和查询,是更好的责任管理方式。 - -对于业务逻辑无关的技术型基础项目,特别是开源的公共项目,应使用 `@author` 标识。 - - -示例: - -```javascript -/** - * @file Describe the file - * @author author-name(mail-name@domain.com) - * author-name2(mail-name2@domain.com) - */ -``` - -#### 2.4.6 命名空间注释 - - -##### [建议] 命名空间使用 `@namespace` 标识。 - -示例: - -```javascript -/** - * @namespace - */ -var util = {}; -``` - -#### 2.4.7 类注释 - +#### 2.4.6 类注释 ##### [建议] 使用 `@class` 标记类或构造函数。 @@ -944,7 +895,6 @@ var util = {}; 对于使用对象 `constructor` 属性来定义的构造函数,可以使用 `@constructor` 来标记。 - 示例: ```javascript @@ -953,8 +903,7 @@ var util = {}; * * @class */ -function Developer() { - // constructor body +class Developer { } ``` @@ -969,19 +918,18 @@ function Developer() { * @class * @extends Developer */ -function Fronteer() { - Developer.call(this); - // constructor body +class Fronteer extends Developer { + constructor() { + super(); + } } -util.inherits(Fronteer, Developer); ``` -##### [强制] 使用包装方式扩展类成员时, 必须通过 `@lends` 进行重新指向。 +##### [强制] 类的属性或方法等成员信息不是 `public` 的,应使用 `@protected` 或 `@private` 标识可访问性。 解释: -没有 `@lends` 标记将无法为该类生成包含扩展类成员的文档。 - +生成的文档中将有可访问性的标记,避免用户直接使用非 `public` 的属性或方法。 示例: @@ -992,99 +940,38 @@ util.inherits(Fronteer, Developer); * @class * @extends Developer */ -function Fronteer() { - Developer.call(this); - // constructor body -} +class Fronteer { + /** + * @type {string} + * @private + */ + value = 'T12'; + + /** + * 方法描述 + * + * @private + * @return {string} 返回值描述 + */ + getLevel() { + return this.value; + }; +}; -util.extend( - Fronteer.prototype, - /** @lends Fronteer.prototype */{ - getLevel: function () { - // TODO - } - } -); ``` -##### [强制] 类的属性或方法等成员信息不是 `public` 的,应使用 `@protected` 或 `@private` 标识可访问性。 +#### 2.4.7 函数/方法注释 + +##### [建议] 函数/方法注释包含函数说明,有参数和返回值时必须使用注释标识。 解释: -生成的文档中将有可访问性的标记,避免用户直接使用非 `public` 的属性或方法。 +当 `return` 关键字仅作退出函数/方法使用时,无须对返回值作注释标识。 -示例: -```javascript -/** - * 类描述 - * - * @class - * @extends Developer - */ -var Fronteer = function () { - Developer.call(this); - - /** - * 属性描述 - * - * @type {string} - * @private - */ - this.level = 'T12'; - - // constructor body -}; -util.inherits(Fronteer, Developer); - -/** - * 方法描述 - * - * @private - * @return {string} 返回值描述 - */ -Fronteer.prototype.getLevel = function () { -}; -``` - - -#### 2.4.8 函数/方法注释 - - -##### [强制] 函数/方法注释必须包含函数说明,有参数和返回值时必须使用注释标识。 - -解释: - -当 `return` 关键字仅作退出函数/方法使用时,无须对返回值作注释标识。 - - -##### [强制] 参数和返回值注释必须包含类型信息,且不允许省略参数的说明。 +##### [建议] 参数和返回值注释包含类型信息,且不允许省略参数的说明。 -##### [建议] 当函数是内部函数,外部不可访问时,可以使用 `@inner` 标识。 - -示例: - -```javascript -/** - * 函数描述 - * - * @param {string} p1 参数1的说明 - * @param {string} p2 参数2的说明,比较长 - * 那就换行了. - * @param {number=} p3 参数3的说明(可选) - * @return {Object} 返回值描述 - */ -function foo(p1, p2, p3) { - var p3 = p3 || 10; - return { - p1: p1, - p2: p2, - p3: p3 - }; -} -``` - -##### [强制] 对 Object 中各项的描述, 必须使用 `@param` 标识。 +##### [建议] 对 `Object` 类型的对象中各项的描述, 使用 `@param` 标识。 示例: @@ -1107,10 +994,9 @@ function foo(option) { 简而言之,当子类重写的方法能直接套用父类的方法注释时可省略对参数与返回值的注释。 -#### 2.4.9 事件注释 +#### 2.4.8 事件注释 - -##### [强制] 必须使用 `@event` 标识事件,事件参数的标识与方法描述的参数标识相同。 +##### [建议] 必须使用 `@event` 标识事件,事件参数的标识与方法描述的参数标识相同。 示例: @@ -1132,7 +1018,7 @@ this.fire( ); ``` -##### [强制] 在会广播事件的函数前使用 `@fires` 标识广播的事件,在广播事件代码前使用 `@event` 标识事件。 +##### [建议] 在会广播事件的函数前使用 `@fires` 标识广播的事件,在广播事件代码前使用 `@event` 标识事件。 ##### [建议] 对于事件对象的注释,使用 `@param` 标识,生成文档时可读性更好。 @@ -1145,30 +1031,30 @@ this.fire( * @fires Select#change * @private */ -Select.prototype.clickHandler = function () { - - /** - * 值变更时触发 - * - * @event Select#change - * @param {Object} e e描述 - * @param {string} e.before before描述 - * @param {string} e.after after描述 - */ - this.fire( - 'change', - { - before: 'foo', - after: 'bar' - } - ); +class Select { + clickHandler() { + /** + * 值变更时触发 + * + * @event Select#change + * @param {Object} e e描述 + * @param {string} e.before before描述 + * @param {string} e.after after描述 + */ + this.fire( + 'change', + { + before: 'foo', + after: 'bar' + } + ); + } }; ``` -#### 2.4.10 常量注释 +#### 2.4.9 常量注释 - -##### [强制] 常量必须使用 `@const` 标记,并包含说明和类型信息。 +##### [建议] 常量使用 `@const` 标记,并包含说明和类型信息。 示例: @@ -1179,10 +1065,10 @@ Select.prototype.clickHandler = function () { * @const * @type {string} */ -var REQUEST_URL = 'myurl.do'; +const REQUEST_URL = 'myurl.do'; ``` -#### 2.4.11 复杂类型注释 +#### 2.4.10 复杂类型注释 ##### [建议] 对于类型未定义的复杂结构的注释,可以使用 `@typedef` 标识来定义。 @@ -1204,7 +1090,7 @@ var REQUEST_URL = 'myurl.do'; * * @type {Array.} */ -var servers = [ +let servers = [ { host: '1.2.3.4', port: 8080 @@ -1216,226 +1102,7 @@ var servers = [ ]; ``` - -#### 2.4.12 AMD 模块注释 - - -##### [强制] AMD 模块使用 `@module` 或 `@exports` 标识。 - -解释: - -@exports 与 @module 都可以用来标识模块,区别在于 @module 可以省略模块名称。而只使用 @exports 时在 namepaths 中可以省略 module: 前缀。 - - -示例: - -```javascript -define( - function (require) { - - /** - * foo description - * - * @exports Foo - */ - var foo = { - // TODO - }; - - /** - * baz description - * - * @return {boolean} return description - */ - foo.baz = function () { - // TODO - }; - - return foo; - - } -); -``` - -也可以在 exports 变量前使用 @module 标识: - -```javascript -define( - function (require) { - - /** - * module description. - * - * @module foo - */ - var exports = {}; - - - /** - * bar description - * - */ - exports.bar = function () { - // TODO - }; - - return exports; - } -); -``` - -如果直接使用 factory 的 exports 参数,还可以: - -```javascript -/** - * module description. - * - * @module - */ -define( - function (require, exports) { - - /** - * bar description - * - */ - exports.bar = function () { - // TODO - }; - return exports; - } -); -``` - -##### [强制] 对于已使用 `@module` 标识为 AMD模块 的引用,在 `namepaths` 中必须增加 `module:` 作前缀。 - -解释: - -namepaths 没有 module: 前缀时,生成的文档中将无法正确生成链接。 - -示例: - -```javascript -/** - * 点击处理 - * - * @fires module:Select#change - * @private - */ -Select.prototype.clickHandler = function () { - /** - * 值变更时触发 - * - * @event module:Select#change - * @param {Object} e e描述 - * @param {string} e.before before描述 - * @param {string} e.after after描述 - */ - this.fire( - 'change', - { - before: 'foo', - after: 'bar' - } - ); -}; -``` - -##### [建议] 对于类定义的模块,可以使用 `@alias` 标识构建函数。 - -示例: - -```javascript -/** - * A module representing a jacket. - * @module jacket - */ -define( - function () { - - /** - * @class - * @alias module:jacket - */ - var Jacket = function () { - }; - - return Jacket; - } -); -``` - - -##### [建议] 多模块定义时,可以使用 `@exports` 标识各个模块。 - -示例: - -```javascript -// one module -define('html/utils', - /** - * Utility functions to ease working with DOM elements. - * @exports html/utils - */ - function () { - var exports = { - }; - - return exports; - } -); - -// another module -define('tag', - /** @exports tag */ - function () { - var exports = { - }; - - return exports; - } -); -``` - -##### [建议] 对于 exports 为 Object 的模块,可以使用`@namespace`标识。 - -解释: - -使用 @namespace 而不是 @module 或 @exports 时,对模块的引用可以省略 module: 前缀。 - -##### [建议] 对于 exports 为类名的模块,使用 `@class` 和 `@exports` 标识。 - - -示例: - -```javascript - -// 只使用 @class Bar 时,类方法和属性都必须增加 @name Bar#methodName 来标识,与 @exports 配合可以免除这一麻烦,并且在引用时可以省去 module: 前缀。 -// 另外需要注意类名需要使用 var 定义的方式。 - -/** - * Bar description - * - * @see foo - * @exports Bar - * @class - */ -var Bar = function () { - // TODO -}; - -/** - * baz description - * - * @return {(string|Array)} return description - */ -Bar.prototype.baz = function () { - // TODO -}; -``` - - -#### 2.4.13 细节注释 +#### 2.4.11 细节注释 对于内部实现、不容易理解的逻辑说明、摘要信息等,我们可能需要编写细节注释。 @@ -1454,40 +1121,37 @@ function foo(p1, p2, opt_p3) { } ``` -##### [强制] 有时我们会使用一些特殊标记进行说明。特殊标记必须使用单行注释的形式。下面列举了一些常用标记: - -解释: - -1. TODO: 有功能待实现。此时需要对将要实现的功能进行简单说明。 -2. FIXME: 该处代码运行没问题,但可能由于时间赶或者其他原因,需要修正。此时需要对如何修正进行简单说明。 -3. HACK: 为修正某些问题而写的不太好或者使用了某些诡异手段的代码。此时需要对思路或诡异手段进行描述。 -4. XXX: 该处存在陷阱。此时需要对陷阱进行描述。 - - - - ## 3 语言特性 +### 3.1 变量 +##### [强制] 变量均使用 `let` 或 `const` 定义,不得使用 `var` 定义变量。 +解释: +`var` 定义的变量作用域过大,有更大的使用风险。 +```javascript +//good +let name = 'MyName'; +const ip = '127.0.0.1'; -### 3.1 变量 - +// bad +let name = 'MyName'; +``` ##### [强制] 变量、函数在使用前必须先定义。 解释: -不通过 var 定义变量将导致变量污染全局环境。 +不得直接向未定义的变量赋值来污染全局环境。 示例: ```javascript // good -var name = 'MyName'; +let name = 'MyName'; // bad name = 'MyName'; @@ -1499,48 +1163,46 @@ name = 'MyName'; ```javascript /* globals jQuery */ -var element = jQuery('#element-id'); +let element = jQuery('#element-id'); ``` -##### [强制] 每个 `var` 只能声明一个变量。 +##### [强制] 每个 `let` 或 `const` 只能声明一个变量。 解释: -一个 `var` 声明多个变量,容易导致较长的行长度,并且在修改时容易造成逗号和分号的混淆。 +一个 `let` 或 `const` 声明多个变量,容易导致较长的行长度,并且在修改时容易造成逗号和分号的混淆。 示例: ```javascript // good -var hangModules = []; -var missModules = []; -var visited = {}; +let hangModules = []; +let missModules = []; +let visited = {}; // bad -var hangModules = [], +let hangModules = [], missModules = [], visited = {}; ``` - ##### [强制] 变量必须 `即用即声明`,不得在函数或其它形式的代码块起始位置统一声明所有变量。 解释: 变量声明与使用的距离越远,出现的跨度越大,代码的阅读与维护成本越高。虽然JavaScript的变量是函数作用域,还是应该根据编程中的意图,缩小变量出现的距离空间。 - 示例: ```javascript // good function kv2List(source) { - var list = []; + let list = []; - for (var key in source) { + for (let key in source) { if (source.hasOwnProperty(key)) { - var item = { + let item = { k: key, v: source[key] }; @@ -1554,9 +1216,9 @@ function kv2List(source) { // bad function kv2List(source) { - var list = []; - var key; - var item; + let list = []; + let key; + let item; for (key in source) { if (source.hasOwnProperty(key)) { @@ -1573,14 +1235,8 @@ function kv2List(source) { } ``` - - - - - ### 3.2 条件 - ##### [强制] 在 Equality Expression 中使用类型严格的 `===`。仅当判断 `null` 或 `undefined` 时,允许使用 `== null`。 解释: @@ -1602,120 +1258,79 @@ if (age == 30) { } ``` -##### [建议] 尽可能使用简洁的表达式。 +##### [建议] 按执行频率排列分支的顺序。 +解释: -示例: +按执行频率排列分支的顺序好处是: -```javascript -// 字符串为空 +1. 阅读的人容易找到最常见的情况,增加可读性。 +2. 提高执行效率。 -// good -if (!name) { - // ...... -} +##### [建议] 在逻辑实现中,考虑尽早返回。 -// bad -if (name === '') { - // ...... -} -``` +解释: -```javascript -// 字符串非空 +将较短的逻辑块放在分支中,并使用 `return` 语句尽早使函数返回,可以有效减少 `if / else if` 的分支数量和代码的缩进量,使得代码整体结构更好、更易阅读。 +```javascript // good -if (name) { - // ...... +function foo(value) { + if (value < 0) { + workWithNegativeValue(value); + return; // 尽早返回 + } + + // 后续不用写 `else` + let workingValue = Math.min(value, 1024); + workWithPositiveValue(workingValue); } // bad -if (name !== '') { - // ...... +function foo(value) { + if (value < 0) { + workWithNegativeValue(value); + } + else { + // 多了一层缩进,代码显得更复杂 + let workingValue = Math.min(value, 1024); + workWithPositiveValue(workingValue); + } } ``` -```javascript -// 数组非空 +##### [建议] 对于相同变量或表达式的多值条件,用 `switch` 代替 `if`。 + +示例: +```javascript // good -if (collection.length) { - // ...... +switch (typeof variable) { + case 'object': + // ...... + break; + case 'number': + case 'boolean': + case 'string': + // ...... + break; } // bad -if (collection.length > 0) { +let type = typeof variable; +if (type === 'object') { // ...... } -``` - -```javascript -// 布尔不成立 - -// good -if (!notTrue) { +else if (type === 'number' || type === 'boolean' || type === 'string') { // ...... } +``` -// bad -if (notTrue === false) { - // ...... -} -``` - -```javascript -// null 或 undefined - -// good -if (noValue == null) { - // ...... -} - -// bad -if (noValue === null || typeof noValue === 'undefined') { - // ...... -} -``` - - -##### [建议] 按执行频率排列分支的顺序。 +##### [建议] 如果函数或全局中的 `else` 块后没有任何语句,可以删除 `else`。 解释: -按执行频率排列分支的顺序好处是: - -1. 阅读的人容易找到最常见的情况,增加可读性。 -2. 提高执行效率。 - - -##### [建议] 对于相同变量或表达式的多值条件,用 `switch` 代替 `if`。 - -示例: - -```javascript -// good -switch (typeof variable) { - case 'object': - // ...... - break; - case 'number': - case 'boolean': - case 'string': - // ...... - break; -} - -// bad -var type = typeof variable; -if (type === 'object') { - // ...... -} -else if (type === 'number' || type === 'boolean' || type === 'string') { - // ...... -} -``` - -##### [建议] 如果函数或全局中的 `else` 块后没有任何语句,可以删除 `else`。 +当一个 `else` 分支前的其它分支均通过 `return` 或 `throw` 等方式中断了函数时,该段逻辑并不需要 `else` 分支包裹,可以通过删除 `else` 部分来减少缩进,提升代码可读性。 示例: @@ -1740,19 +1355,17 @@ function getName() { } ``` - - - - ### 3.3 循环 -##### [建议] 不要在循环体中包含函数表达式,事先将函数提取到循环体外。 +##### [建议] 如无必要,不要在循环体中包含函数表达式,事先将函数提取到循环体外。 解释: 循环体中的函数表达式,运行过程中会生成循环次数个函数对象。 +仅在依赖循环中的闭包变量,如 `i` 或 `length` 时,需要在循环体中声明函数。 + 示例: @@ -1762,84 +1375,49 @@ function clicker() { // ...... } -for (var i = 0, len = elements.length; i < len; i++) { - var element = elements[i]; +for (let i = 0; i < elements.length; i++) { + let element = elements[i]; addListener(element, 'click', clicker); } // bad -for (var i = 0, len = elements.length; i < len; i++) { - var element = elements[i]; +for (let i = 0; i < elements.length; i++) { + let element = elements[i]; addListener(element, 'click', function () {}); } ``` -##### [建议] 对循环内多次使用的不变值,在循环外用变量缓存。 +##### [建议] 对循环内多次使用的高访问成本的不变值,在循环外用变量缓存。 + +解释: + +在 DOM 等环境下,部分属性的访问有很高的成本,如 `offsetWidth` 会引起重排,此时需要将该值放在循环体外缓存,避免多次读取产生大量的性能损失。 示例: ```javascript // good -var width = wrap.offsetWidth + 'px'; -for (var i = 0, len = elements.length; i < len; i++) { - var element = elements[i]; +let width = wrap.offsetWidth + 'px'; +for (let i = 0; i < elements.length; i++) { + let element = elements[i]; element.style.width = width; // ...... } // bad -for (var i = 0, len = elements.length; i < len; i++) { - var element = elements[i]; +for (let i = 0; i < elements.length; i++) { + let element = elements[i]; element.style.width = wrap.offsetWidth + 'px'; // ...... } ``` - -##### [建议] 对有序集合进行遍历时,缓存 `length`。 - -解释: - -虽然现代浏览器都对数组长度进行了缓存,但对于一些宿主对象和老旧浏览器的数组对象,在每次 `length` 访问时会动态计算元素个数,此时缓存 `length` 能有效提高程序性能。 - - -示例: - -```javascript -for (var i = 0, len = elements.length; i < len; i++) { - var element = elements[i]; - // ...... -} -``` - -##### [建议] 对有序集合进行顺序无关的遍历时,使用逆序遍历。 - -解释: - -逆序遍历可以节省变量,代码比较优化。 - -示例: - -```javascript -var len = elements.length; -while (len--) { - var element = elements[len]; - // ...... -} -``` - - - - - ### 3.4 类型 - #### 3.4.1 类型检测 - ##### [建议] 类型检测优先使用 `typeof`。对象类型检测使用 `instanceof`。`null` 或 `undefined` 的检测使用 `== null`。 示例: @@ -1876,22 +1454,20 @@ variable == null typeof variable === 'undefined' ``` - #### 3.4.2 类型转换 - -##### [建议] 转换成 `string` 时,使用 `+ ''`。 +##### [建议] 转换成 `string` 时,使用 `String(value)` 、 `toString()` 或模板字符串。 示例: ```javascript // good -num + ''; +String(number); +number.toString(); +`${number}`; // bad -new String(num); -num.toString(); -String(num); +number + ''; ``` ##### [建议] 转换成 `number` 时,通常使用 `+`。 @@ -1911,7 +1487,7 @@ Number(str); 示例: ```javascript -var width = '200px'; +let width = '200px'; parseInt(width, 10); ``` @@ -1932,30 +1508,26 @@ parseInt(str); 示例: ```javascript -var num = 3.14; +let num = 3.14; !!num; ``` -##### [建议] `number` 去除小数点,使用 `Math.floor` / `Math.round` / `Math.ceil`,不使用 `parseInt`。 +##### [建议] `number` 去除小数点,使用 `Math.floor` / `Math.round` / `Math.ceil` / `Math.trunc`,不使用 `parseInt`。 示例: ```javascript // good -var num = 3.14; +let num = 3.14; Math.ceil(num); // bad -var num = 3.14; +let num = 3.14; parseInt(num, 10); ``` - - - ### 3.5 字符串 - ##### [强制] 字符串开头和结束使用单引号 `'`。 解释: @@ -1963,98 +1535,90 @@ parseInt(num, 10); 1. 输入单引号不需要按住 `shift`,方便输入。 2. 实际使用中,字符串经常用来拼接 HTML。为方便 HTML 中包含双引号而不需要转义写法。 +在字符串本身包含单引号(`'`)时,则可以使用模板字符串代替。 + 示例: ```javascript -var str = '我是一个字符串'; -var html = '
    拼接HTML可以省去双引号转义
    '; +let str = '我是一个字符串'; +let html = '
    拼接HTML可以省去双引号转义
    '; +let code = `console.log('Hello World')`; ``` -##### [建议] 使用 `数组` 或 `+` 拼接字符串。 +##### [建议] 在合适场景下,使用模板字符串、 `数组` 或 `+` 拼接字符串。 解释: 1. 使用 `+` 拼接字符串,如果拼接的全部是 StringLiteral,压缩工具可以对其进行自动合并的优化。所以,静态字符串建议使用 `+` 拼接。 -2. 在现代浏览器下,使用 `+` 拼接字符串,性能较数组的方式要高。 -3. 如需要兼顾老旧浏览器,应尽量使用数组拼接字符串。 +2. 对于存在变量的、有多行的,可以使用模板字符串。 +3. 对于每一小块显著有语义性和关联性,并通过统一的字符连接的,可以使用数组。 示例: ```javascript // 使用数组拼接字符串 -var str = [ +let elements = [ // 推荐换行开始并缩进开始第一个字符串, 对齐代码, 方便阅读. '' -].join(''); +]; +let html = elements.join(''); + +// 配合 `dedent` 使用模板字符串 +let html = dedent` + +`; // 使用 `+` 拼接字符串 -var str2 = '' // 建议第一个为空字符串, 第二个换行开始并缩进开始, 对齐代码, 方便阅读 +let html = '' // 建议第一个为空字符串, 第二个换行开始并缩进开始, 对齐代码, 方便阅读 + ''; ``` -##### [建议] 使用字符串拼接的方式生成HTML,需要根据语境进行合理的转义。 +##### [强制] 使用字符串拼接的方式生成HTML,需要根据语境进行合理的转义。 解释: -在 `JavaScript` 中拼接,并且最终将输出到页面中的字符串,需要进行合理转义,以防止安全漏洞。下面的示例代码为场景说明,不能直接运行。 - +在 JavaScript 中拼接,并且最终将输出到页面中的字符串,需要进行合理转义,以防止安全漏洞。下面的示例代码为场景说明,不能直接运行。 示例: ```javascript // HTML 转义 -var str = '

    ' + htmlEncode(content) + '

    '; +let str = '

    ' + htmlEncode(content) + '

    '; // HTML 转义 -var str = ''; +let str = ''; // URL 转义 -var str = 'link'; +let str = 'link'; // JavaScript字符串 转义 + HTML 转义 -var str = ''; +let str = ''; ``` - -##### [建议] 复杂的数据到视图字符串的转换过程,选用一种模板引擎。 - -解释: - -使用模板引擎有如下好处: - -1. 在开发过程中专注于数据,将视图生成的过程由另外一个层级维护,使程序逻辑结构更清晰。 -2. 优秀的模板引擎,通过模板编译技术和高质量的编译产物,能获得比手工拼接字符串更高的性能。 -3. 模板引擎能方便的对动态数据进行相应的转义,部分模板引擎默认进行HTML转义,安全性更好。 - -- artTemplate: 体积较小,在所有环境下性能高,语法灵活。 -- dot.js: 体积小,在现代浏览器下性能高,语法灵活。 -- etpl: 体积较小,在所有环境下性能高,模板复用性高,语法灵活。 -- handlebars: 体积大,在所有环境下性能高,扩展性高。 -- hogon: 体积小,在现代浏览器下性能高。 -- nunjucks: 体积较大,性能一般,模板复用性高。 - - - - ### 3.6 对象 ##### [强制] 使用对象字面量 `{}` 创建新 `Object`。 +在特殊情况下,可以使用 `Object.create(null)` 创建一个特殊的没有原型的对象。 + 示例: ```javascript // good -var obj = {}; +let obj = {}; // bad -var obj = new Object(); +let obj = new Object(); ``` ##### [建议] 对象创建时,如果一个对象的所有 `属性` 均可以不添加引号,建议所有 `属性` 不添加引号。 @@ -2062,37 +1626,12 @@ var obj = new Object(); 示例: ```javascript -var info = { +let info = { name: 'someone', age: 28 }; ``` -##### [建议] 对象创建时,如果任何一个 `属性` 需要添加引号,则所有 `属性` 建议添加 `'`。 - -解释: - -如果属性不符合 Identifier 和 NumberLiteral 的形式,就需要以 StringLiteral 的形式提供。 - - -示例: - -```javascript -// good -var info = { - 'name': 'someone', - 'age': 28, - 'more-info': '...' -}; - -// bad -var info = { - name: 'someone', - age: 28, - 'more-info': '...' -}; -``` - ##### [强制] 不允许修改和扩展任何原生对象和宿主对象的原型。 示例: @@ -2124,17 +1663,14 @@ info['more-info']; 示例: ```javascript -var newInfo = {}; -for (var key in info) { +let newInfo = {}; +for (let key in info) { if (info.hasOwnProperty(key)) { newInfo[key] = info[key]; } } ``` - - - ### 3.7 数组 @@ -2144,10 +1680,10 @@ for (var key in info) { ```javascript // good -var arr = []; +let arr = []; // bad -var arr = new Array(); +let arr = new Array(); ``` ##### [强制] 遍历数组不使用 `for in`。 @@ -2159,18 +1695,18 @@ var arr = new Array(); 示例: ```javascript -var arr = ['a', 'b', 'c']; +let arr = ['a', 'b', 'c']; // 这里仅作演示, 实际中应使用 Object 类型 arr.other = 'other things'; // 正确的遍历方式 -for (var i = 0, len = arr.length; i < len; i++) { +for (let i = 0, len = arr.length; i < len; i++) { console.log(i); } // 错误的遍历方式 -for (var i in arr) { +for (let i in arr) { console.log(i); } ``` @@ -2179,24 +1715,13 @@ for (var i in arr) { 解释: -自己实现的常规排序算法,在性能上并不优于数组默认的 `sort` 方法。以下两种场景可以自己实现排序: - -1. 需要稳定的排序算法,达到严格一致的排序结果。 -2. 数据特点鲜明,适合使用桶排。 - -##### [建议] 清空数组使用 `.length = 0`。 - - - +自己实现的常规排序算法,在性能上并不优于数组默认的,当数据特点鲜明,适合使用桶排时,才考虑使用自定义的排序实现。 ### 3.8 函数 - - #### 3.8.1 函数长度 - -##### [建议] 一个函数的长度控制在 `50` 行以内。 +##### [建议] 一个函数的长度控制在 **50** 行以内。 解释: @@ -2252,18 +1777,22 @@ function checkAAvailability() { } ``` +##### [建议] 一个函数的缩进层级小于 **6** 层。 -#### 3.8.2 参数设计 +解释: +过多的层级代表着函数复杂度太大,需要合理拆解函数。 -##### [建议] 一个函数的参数控制在 `6` 个以内。 +包含JSX的部分可以有限地超过 6 层,但依然要有一个合理的限制,不应有过深的嵌套。 + +#### 3.8.2 参数设计 -解释: -除去不定长参数以外,函数具备不同逻辑意义的参数建议控制在 `6` 个以内,过多参数会导致维护难度增大。 +##### [建议] 一个函数的参数控制在 **6**** 个以内。 -某些情况下,如使用 AMD Loader 的 `require` 加载多个模块时,其 `callback` 可能会存在较多参数,因此对函数参数的个数不做强制限制。 +解释: +除去不定长参数以外,函数具备不同逻辑意义的参数建议控制在 **6** 个以内,过多参数会导致维护难度增大。 ##### [建议] 通过 `options` 参数传递非数据输入型参数。 @@ -2314,95 +1843,14 @@ function removeElement(element, options) { - 当配置项有增长时,无需无休止地增加参数个数,不会出现 `removeElement(element, true, false, false, 3)` 这样难以理解的调用代码。 - 当部分配置参数可选时,多个参数的形式非常难处理重载逻辑,而使用一个 options 对象只需判断属性是否存在,实现得以简化。 - - -#### 3.8.3 闭包 - - -##### [建议] 在适当的时候将闭包内大对象置为 `null`。 - -解释: - -在 JavaScript 中,无需特别的关键词就可以使用闭包,一个函数可以任意访问在其定义的作用域外的变量。需要注意的是,函数的作用域是静态的,即在定义时决定,与调用的时机和方式没有任何关系。 - -闭包会阻止一些变量的垃圾回收,对于较老旧的 JavaScript 引擎,可能导致外部所有变量均无法回收。 - -首先一个较为明确的结论是,以下内容会影响到闭包内变量的回收: - -- 嵌套的函数中是否有使用该变量。 -- 嵌套的函数中是否有 **直接调用eval**。 -- 是否使用了 with 表达式。 - -Chakra、V8 和 SpiderMonkey 将受以上因素的影响,表现出不尽相同又较为相似的回收策略,而 JScript.dll 和 Carakan 则完全没有这方面的优化,会完整保留整个 LexicalEnvironment 中的所有变量绑定,造成一定的内存消耗。 - -由于对闭包内变量有回收优化策略的 Chakra、V8 和 SpiderMonkey 引擎的行为较为相似,因此可以总结如下,当返回一个函数 **fn** 时: - -1. 如果 **fn** 的 `[[Scope]]` 是 ObjectEnvironment(with 表达式生成 ObjectEnvironment,函数和 catch 表达式生成 DeclarativeEnvironment),则: - 1. 如果是 V8 引擎,则退出全过程。 - 2. 如果是 SpiderMonkey,则处理该 ObjectEnvironment 的外层 LexicalEnvironment。 -2. 获取当前 LexicalEnvironment 下的所有类型为 Function 的对象,对于每一个 Function 对象,分析其 FunctionBody: - 1. 如果 FunctionBody 中含有 **直接调用 eval**,则退出全过程。 - 2. 否则得到所有的 Identifier。 - 3. 对于每一个 Identifier,设其为 **name**,根据查找变量引用的规则,从 LexicalEnvironment 中找出名称为 **name** 的绑定 binding。 - 4. 对 binding 添加 **notSwap** 属性,其值为 `true`。 -3. 检查当前 LexicalEnvironment 中的每一个变量绑定,如果该绑定有 **notSwap** 属性且值为 `true`,则: - 1. 如果是 V8 引擎,删除该绑定。 - 2. 如果是 SpiderMonkey,将该绑定的值设为 `undefined`,将删除 **notSwap** 属性。 - -对于 Chakra 引擎,暂无法得知是按 V8 的模式还是按 SpiderMonkey 的模式进行。 - -如果有 **非常庞大** 的对象,且预计会在 **老旧的引擎** 中执行,则使用闭包时,注意将闭包不需要的对象置为空引用。 - -##### [建议] 使用 `IIFE` 避免 `Lift 效应`。 - -解释: - -在引用函数外部变量时,函数执行时外部变量的值由运行时决定而非定义时,最典型的场景如下: - -```javascript -var tasks = []; -for (var i = 0; i < 5; i++) { - tasks[tasks.length] = function () { - console.log('Current cursor is at ' + i); - }; -} - -var len = tasks.length; -while (len--) { - tasks[len](); -} -``` - -以上代码对 tasks 中的函数的执行均会输出 `Current cursor is at 5`,往往不符合预期。 - -此现象称为 **Lift 效应** 。解决的方式是通过额外加上一层闭包函数,将需要的外部变量作为参数传递来解除变量的绑定关系: - -```javascript -var tasks = []; -for (var i = 0; i < 5; i++) { - // 注意有一层额外的闭包 - tasks[tasks.length] = (function (i) { - return function () { - console.log('Current cursor is at ' + i); - }; - })(i); -} - -var len = tasks.length; -while (len--) { - tasks[len](); -} -``` - -#### 3.8.4 空函数 - +#### 3.8.3 空函数 ##### [建议] 空函数不使用 `new Function()` 的形式。 示例: ```javascript -var emptyFunction = function () {}; +let emptyFunction = function () {}; ``` ##### [建议] 对于性能有高要求的场合,建议存在一个空函数的常量,供多处使用共享。 @@ -2410,98 +1858,27 @@ var emptyFunction = function () {}; 示例: ```javascript -var EMPTY_FUNCTION = function () {}; +const EMPTY_FUNCTION = function () {}; -function MyClass() { +function dispose() { + source.setProgressHandler(EMPTY_FUNCTION); + source.setFinishHandler(EMPTY_FUNCTION); } - -MyClass.prototype.abstractMethod = EMPTY_FUNCTION; -MyClass.prototype.hooks.before = EMPTY_FUNCTION; -MyClass.prototype.hooks.after = EMPTY_FUNCTION; ``` - - - - - - ### 3.9 面向对象 - -##### [强制] 类的继承方案,实现时需要修正 `constructor`。 - -解释: - -通常使用其他 library 的类继承方案都会进行 `constructor` 修正。如果是自己实现的类继承方案,需要进行 `constructor` 修正。 - - -示例: - -```javascript -/** - * 构建类之间的继承关系 - * - * @param {Function} subClass 子类函数 - * @param {Function} superClass 父类函数 - */ -function inherits(subClass, superClass) { - var F = new Function(); - F.prototype = superClass.prototype; - subClass.prototype = new F(); - subClass.prototype.constructor = subClass; -} -``` - -##### [建议] 声明类时,保证 `constructor` 的正确性。 - -示例: - -```javascript -function Animal(name) { - this.name = name; -} - -// 直接prototype等于对象时,需要修正constructor -Animal.prototype = { - constructor: Animal, - - jump: function () { - alert('animal ' + this.name + ' jump'); - } -}; - -// 这种方式扩展prototype则无需理会constructor -Animal.prototype.jump = function () { - alert('animal ' + this.name + ' jump'); -}; -``` - - -##### [建议] 属性在构造函数中声明,方法在原型中声明。 +##### [强制] 自定义事件的事件名就当与对应框架相符。 解释: -原型对象的成员被所有实例共享,能节约内存占用。所以编码时我们应该遵守这样的原则:原型对象包含程序不会修改的成员,如方法函数或配置项。 - -```javascript -function TextNode(value, engine) { - this.value = value; - this.engine = engine; -} - -TextNode.prototype.clone = function () { - return this; -}; -``` +当使用某个框架时,API 设计中的自定义事件名也就当符合该框架官方事件的命名,一些参考: -##### [强制] 自定义事件的 `事件名` 必须全小写。 +- DOM:使用全小写,如`dragstart`。 +- NodeJS:使用 `camelCase` 的形式,如 `unhandledRejection` 。 +- VSCode:使用 `camelCase` 的形式,如 `onDebugResolve` 。 -解释: - -在 JavaScript 广泛应用的浏览器环境,绝大多数 DOM 事件名称都是全小写的。为了遵循大多数 JavaScript 开发者的习惯,在设计自定义事件时,事件名也应该全小写。 - -##### [强制] 自定义事件只能有一个 `event` 参数。如果事件需要传递较多信息,应仔细设计事件对象。 +##### [建议] 自定义事件只能有一个 `event` 参数。如果事件需要传递较多信息,应仔细设计事件对象。 解释: @@ -2511,25 +1888,10 @@ TextNode.prototype.clone = function () { 2. 每个事件信息都可以根据需要提供或者不提供,更自由。 3. 扩展方便,未来添加事件信息时,无需考虑会破坏监听器参数形式而无法向后兼容。 - -##### [建议] 设计自定义事件时,应考虑禁止默认行为。 - -解释: - -常见禁止默认行为的方式有两种: - -1. 事件监听函数中 `return false`。 -2. 事件对象中包含禁止默认行为的方法,如 `preventDefault`。 - - - - ### 3.10 动态特性 - #### 3.10.1 eval - ##### [强制] 避免使用直接 `eval` 函数。 解释: @@ -2543,7 +1905,6 @@ TextNode.prototype.clone = function () { #### 3.10.2 动态执行代码 - ##### [建议] 使用 `new Function` 执行动态代码。 解释: @@ -2554,59 +1915,28 @@ TextNode.prototype.clone = function () { 示例: ```javascript -var handler = new Function('x', 'y', 'return x + y;'); -var result = handler($('#x').val(), $('#y').val()); +let handler = new Function('x', 'y', 'return x + y;'); +let result = handler($('#x').val(), $('#y').val()); ``` - - #### 3.10.3 with - -##### [建议] 尽量不要使用 `with`。 +##### [强制] 避免使用 `with` 关键字。 解释: 使用 `with` 可能会增加代码的复杂度,不利于阅读和管理;也会对性能有影响。大多数使用 `with` 的场景都能使用其他方式较好的替代。所以,尽量不要使用 `with`。 - - - #### 3.10.4 delete - ##### [建议] 减少 `delete` 的使用。 解释: 如果没有特别的需求,减少或避免使用 `delete`。`delete` 的使用会破坏部分 JavaScript 引擎的性能优化。 - -##### [建议] 处理 `delete` 可能产生的异常。 - -解释: - -对于有被遍历需求,且值 `null` 被认为具有业务逻辑意义的值的对象,移除某个属性必须使用 `delete` 操作。 - -在严格模式或 IE 下使用 `delete` 时,不能被删除的属性会抛出异常,因此在不确定属性是否可以删除的情况下,建议添加 `try-catch` 块。 - -示例: - -```javascript -try { - delete o.x; -} -catch (deleteError) { - o.x = null; -} -``` - - - #### 3.10.5 对象属性 - - ##### [建议] 避免修改外部传入的对象。 解释: @@ -2615,226 +1945,65 @@ JavaScript 因其脚本语言的动态特性,当一个对象未被 seal 或 fr 但是随意地对 非自身控制的对象 进行修改,很容易造成代码在不可预知的情况下出现问题。因此,设计良好的组件、函数应该避免对外部传入的对象的修改。 -下面代码的 **selectNode** 方法修改了由外部传入的 **datasource** 对象。如果 **datasource** 用在其它场合(如另一个 Tree 实例)下,会造成状态的混乱。 +下面代码的 **selectNode** 方法修改了由外部传入的 **dataSource** 对象。如果 **datasource** 用在其它场合(如另一个 Tree 实例)下,会造成状态的混乱。 ```javascript -function Tree(datasource) { - this.datasource = datasource; -} - -Tree.prototype.selectNode = function (id) { - // 从datasource中找出节点对象 - var node = this.findNode(id); - if (node) { - node.selected = true; - this.flushView(); - } -}; -``` - -对于此类场景,需要使用额外的对象来维护,使用由自身控制,不与外部产生任何交互的 **selectedNodeIndex** 对象来维护节点的选中状态,不对 **datasource** 作任何修改。 - -```javascript -function Tree(datasource) { - this.datasource = datasource; - this.selectedNodeIndex = {}; -} - -Tree.prototype.selectNode = function (id) { - - // 从datasource中找出节点对象 - var node = this.findNode(id); - - if (node) { - this.selectedNodeIndex[id] = true; - this.flushView(); +class Tree { + constructor(dataSource) { + this.dataSource = dataSource; } -}; -``` - -除此之外,也可以通过 deepClone 等手段将自身维护的对象与外部传入的分离,保证不会相互影响。 - - -##### [建议] 具备强类型的设计。 - -解释: - -- 如果一个属性被设计为 `boolean` 类型,则不要使用 `1` 或 `0` 作为其值。对于标识性的属性,如对代码体积有严格要求,可以从一开始就设计为 `number` 类型且将 `0` 作为否定值。 -- 从 DOM 中取出的值通常为 `string` 类型,如果有对象或函数的接收类型为 `number` 类型,提前作好转换,而不是期望对象、函数可以处理多类型的值。 - - - - - - - - - -## 4 浏览器环境 - - - - -### 4.1 模块化 - - -#### 4.1.1 AMD - - -##### [强制] 使用 `AMD` 作为模块定义。 - -解释: - -AMD 作为由社区认可的模块定义形式,提供多种重载提供灵活的使用方式,并且绝大多数优秀的 Library 都支持 AMD,适合作为规范。 - -目前,比较成熟的 AMD Loader 有: - -- 官方实现的 [requirejs](http://requirejs.org/) -- 百度自己实现的 [esl](https://github.com/ecomfe/esl) - - -##### [强制] 模块 `id` 必须符合标准。 - -解释: - -模块 id 必须符合以下约束条件: - -1. 类型为 string,并且是由 `/` 分割的一系列 terms 来组成。例如:`this/is/a/module`。 -2. term 应该符合 [a-zA-Z0-9_-]+ 规则。 -3. 不应该有 .js 后缀。 -4. 跟文件的路径保持一致。 - - - -#### 4.1.2 define - - -##### [建议] 定义模块时不要指明 `id` 和 `dependencies`。 - -解释: - -在 AMD 的设计思想里,模块名称是和所在路径相关的,匿名的模块更利于封包和迁移。模块依赖应在模块定义内部通过 `local require` 引用。 - -所以,推荐使用 `define(factory)` 的形式进行模块定义。 - - -示例: - -```javascript -define( - function (require) { + selectNode(id) { + // 从dataSource中找出节点对象 + let node = this.findNode(id); + if (node) { + node.selected = true; + this.flushView(); + } } -); +} ``` - -##### [建议] 使用 `return` 来返回模块定义。 - -解释: - -使用 return 可以减少 factory 接收的参数(不需要接收 exports 和 module),在没有 AMD Loader 的场景下也更容易进行简单的处理来伪造一个 Loader。 - -示例: +对于此类场景,需要使用额外的对象来维护,使用由自身控制,不与外部产生任何交互的 **selectedNodeIndex** 对象来维护节点的选中状态,不对 **dataSource** 作任何修改。 ```javascript -define( - function (require) { - var exports = {}; - - // ... - - return exports; +class Tree { + constructor(dataSource) { + this.dataSource = dataSource; + this.selectedNodeIndex = {}; } -); -``` - - - - -#### 4.1.3 require - - -##### [强制] 全局运行环境中,`require` 必须以 `async require` 形式调用。 - -解释: - -模块的加载过程是异步的,同步调用并无法保证得到正确的结果。 - -示例: -```javascript -// good -require(['foo'], function (foo) { -}); - -// bad -var foo = require('foo'); -``` - -##### [强制] 模块定义中只允许使用 `local require`,不允许使用 `global require`。 + selectNode(id) { + // 从dataSource中找出节点对象 + let node = this.findNode(id); -解释: - -1. 在模块定义中使用 `global require`,对封装性是一种破坏。 -2. 在 AMD 里,`global require` 是可以被重命名的。并且 Loader 甚至没有全局的 `require` 变量,而是用 Loader 名称做为 `global require`。模块定义不应该依赖使用的 Loader。 - - -##### [强制] Package 在实现时,内部模块的 `require` 必须使用 `relative id`。 - -解释: - -对于任何可能通过 发布-引入 的形式复用的第三方库、框架、包,开发者所定义的名称不代表使用者使用的名称。因此不要基于任何名称的假设。在实现源码中,`require` 自身的其它模块时使用 `relative id`。 - -示例: - -```javascript -define( - function (require) { - var util = require('./util'); - } -); -``` - - -##### [建议] 不会被调用的依赖模块,在 `factory` 开始处统一 `require`。 - -解释: - -有些模块是依赖的模块,但不会在模块实现中被直接调用,最为典型的是 `css` / `js` / `tpl` 等 Plugin 所引入的外部内容。此类内容建议放在模块定义最开始处统一引用。 - -示例: - -```javascript -define( - function (require) { - require('css!foo.css'); - require('tpl!bar.tpl.html'); - - // ... + if (node) { + this.selectedNodeIndex[id] = true; + this.flushView(); + } } -); +} ``` +除此之外,也可以通过 `deepClone` 等手段将自身维护的对象与外部传入的分离,保证不会相互影响。 +## 4 浏览器环境 -### 4.2 DOM - - -#### 4.2.1 元素获取 +### 4.1 DOM +#### 4.1.1 元素获取 ##### [建议] 对于单个元素,尽可能使用 `document.getElementById` 获取,避免使用`document.all`。 - ##### [建议] 对于多个元素的集合,尽可能使用 `context.getElementsByTagName` 获取。其中 `context` 可以为 `document` 或其他元素。指定 `tagName` 参数为 `*` 可以获得所有子元素。 -##### [建议] 遍历元素集合时,尽量缓存集合长度。如需多次操作同一集合,则应将集合转为数组。 +##### [建议] 操作一个活性的元素集合时,尽量缓存集合长度。如需多次操作同一集合,则应将集合转为数组。 解释: 原生获取元素集合的结果并不直接引用 DOM 元素,而是对索引进行读取,所以 DOM 结构的改变会实时反映到结果中。 +你可以使用 `querySelectorAll` 来获得静态的元素集合,也可以避免此类问题。 示例: @@ -2843,13 +2012,13 @@ define( ``` - ##### [建议] 获取元素的直接子元素时使用 `children`。避免使用`childNodes`,除非预期是需要包含文本、注释和属性类型的节点。 - - - -#### 4.2.2 样式获取 - +#### 4.1.2 样式获取 ##### [建议] 获取元素实际样式信息时,应使用 `getComputedStyle` 或 `currentStyle`。 @@ -2872,10 +2036,7 @@ alert(elements[0].tagName); 通过 style 只能获得内联定义或通过 JavaScript 直接设置的样式。通过 CSS class 设置的元素样式无法直接通过 style 获取。 - - - -#### 4.2.3 样式设置 +#### 4.1.3 样式设置 ##### [建议] 尽可能通过为元素添加预定义的 className 来改变元素样式,避免直接操作 style 设置。 @@ -2886,11 +2047,7 @@ alert(elements[0].tagName); 除了 IE,标准浏览器会忽略不规范的属性值,导致兼容性问题。 - - - -#### 4.2.4 DOM 操作 - +#### 4.1.4 DOM 操作 ##### [建议] 操作 `DOM` 时,尽量减少页面 `reflow`。 @@ -2903,7 +2060,6 @@ alert(elements[0].tagName); - Resize浏览器窗口、滚动页面。 - 读取元素的某些属性(offsetLeft、offsetTop、offsetHeight、offsetWidth、scrollTop/Left/Width/Height、clientTop/Left/Width/Height、getComputedStyle()、currentStyle(in IE)) 。 - ##### [建议] 尽量减少 `DOM` 操作。 解释: @@ -2915,9 +2071,6 @@ DOM 操作也是非常耗时的一种操作,减少 DOM 操作有助于提高 第一种方法看起来比较标准,但是每次循环都会对 DOM 进行操作,性能极低。在这里推荐使用第二种方法。 - - - #### 4.2.5 DOM 事件 @@ -2927,15 +2080,4 @@ DOM 操作也是非常耗时的一种操作,减少 DOM 操作有助于提高 expando 属性绑定事件容易导致互相覆盖。 - -##### [建议] 使用 `addEventListener` 时第三个参数使用 `false`。 - -解释: - -标准浏览器中的 addEventListener 可以通过第三个参数指定两种时间触发模型:冒泡和捕获。而 IE 的 attachEvent 仅支持冒泡的事件触发。所以为了保持一致性,通常 addEventListener 的第三个参数都为 false。 - - ##### [建议] 在没有事件自动管理的框架支持下,应持有监听器函数的引用,在适当时候(元素释放、页面卸载等)移除添加的监听器。 - - - diff --git a/react-style-guide.md b/react-style-guide.md index 13e34a5..cc0e4e8 100644 --- a/react-style-guide.md +++ b/react-style-guide.md @@ -1,459 +1,809 @@ -# React规范 +## 1 文件组织 -## 文件组织 +### 1.1 命名 -- [强制]同一目录下不得拥有同名的`.js`和`.jsx`文件。 +#### [强制] 同一目录下不得拥有同名的`.js` 、 `.jsx` 、 `.ts` 、 `.tsx` 文件。 - 在使用模块导入时,倾向于不添加后缀,如果存在同名但不同后缀的文件,构建工具将无法决定哪一个是需要引入的模块。 +#### [强制] 同一项目下组件文件名保持一致的规则。 -- [强制]组件文件使用一致的`.js`或 `.jsx`后缀。 +以组件名为 `FooBar` 为例,常见的方式有: - 所有组件文件的后缀名从`.js`或`.jsx`中任选其一。 +- `foo-bar.tsx` +- `FooBar.tsx` +- `fooBar.tsx` - 不应在项目中出现部分组件为`.js`文件,部分为`.jsx`的情况。 +解释: -- [强制]每一个文件以`export default`的形式暴露一个组件。 +在使用模块导入时,往往不添加后缀,如果存在同名但不同后缀的文件,构建工具将无法决定哪一个是需要引入的模块。 - 允许一个文件中存在多个不同的组件,但仅允许通过`export default`暴露一个组件,其它组件均定义为内部组件。 +### 1.2 模块化 -- [强制]每个存放组件的目录使用一个`index.js`以命名导出的形式暴露所有组件。 +#### [建议] 实现单个组件的文件以`export default`的形式暴露一个组件。 - 同目录内的组件相互引用使用`import Foo from './Foo';`进行。 +解释: - 引用其它目录的组件使用`import {Foo} from '../component';`进行。 +允许一个文件中存在多个不同的组件,但仅允许通过`export default`暴露一个组件,其它组件均定义为内部组件。 - 建议使用[VSCode的export-index插件](https://marketplace.visualstudio.com/items?itemName=BrunoLM.export-index)等插件自动生成`index.js`的内容。 +## 2 命名规则 -## 命名规则 +### 2.1 组件 -- [强制]组件名为PascalCase。 +#### [强制] 组件名为 `PascalCase`。 - 包括函数组件,名称均为PascalCase。 +包括函数组件,名称均为 `PascalCase`。 -- [强制]组件名称与文件名称保持相同。 +示例: - 同时组件名称应当能体现出组件的功能,以便通过观察文件名即确定使用哪一个组件。 +```jsx +function FooBar() { + return
    ; +} -- [强制]高阶组件使用camelCase命名。 +class FooBar { + render() { + return
    ; + } +} +``` - 高阶组件事实上并非一个组件,而是一个“生成组件类型”的函数,因此遵守JavaScript函数命名的规范,使用camelCase命名。 +#### [强制] 组件名称与文件名称保持相同含义。 -- [强制]使用`onXxx`形式作为`props`中用于回调的属性名称。 +同时组件名称应当能体现出组件的功能,以便通过观察文件名即确定使用哪一个组件。 - 使用统一的命名规则用以区分`props`中回调和非回调部分的属性,在JSX上可以清晰地看到一个组件向上和向下的逻辑交互。 +### 2.2 属性 - 对于不用于回调的函数类型的属性,使用动词作为属性名称。 +#### [强制] 使用`onXxx`形式作为`props`中用于回调的属性名称。 - ```javascript - // onClick作为回调以on开头,renderText非回调函数则使用动词 - let Label = ({onClick, renderText}) => {renderText()}; - ``` +解释: -- [建议]使用`withXxx`或`xxxable`形式的词作为高阶组件的名称。 +使用统一的命名规则用以区分`props`中回调和非回调部分的属性,在JSX上可以清晰地看到一个组件向上和向下的逻辑交互。 - 高阶组件是为组件添加行为和功能的函数,因此使用如上形式的词有助于对其功能进行理解。 +对于不用于回调的函数类型的属性,使用动词作为属性名称。 -- [建议]作为组件方法的事件处理函数以具备业务含义的词作为名称,不使用`onXxx`形式命名。 +示例: - ```javascript - // Good - class Form { - @autobind - collectAndSubmitData() { - let data = { - name: this.state.name, - age: this.state.age - }; - this.props.onSubmit(data); - } +```js +// onClick作为回调以on开头,renderText非回调函数则使用动词 +function Label({onClick, renderText}) { + return {renderText()}; +} +``` - @autobind - syncName() { - // ... - } +#### [建议] 作为组件方法的事件处理函数以具备业务含义的词作为名称,不使用`onXxx`形式命名。 - @autobind - syncAge() { - // ... - } +示例: - render() { - return ( -
    - - - -
    - ); - } +```js +// good +function Form({onSubmit}) { + let [name, setName] = useState(''); + let [age, setAge] = useState(''); + let collectAndSubmitData = () => { + let data = {name, age}; + onSubmit(data); } - ``` + let syncName = e => setName(e.target.value); + let syncAge = e => setAge(e.target.value); -## 组件声明 + return ( +
    + + + +
    + ); +} +``` -- [强制]使用ES Class声明组件,禁止使用`React.createClass`。 +### 2.3 高阶组件 - [React v15.5.0](https://facebook.github.io/react/blog/2017/04/07/react-v15.5.0.html)已经弃用了`React.createClass`函数。 +#### [强制] 高阶组件使用camelCase命名。 - ```javascript - // Bad - let Message = React.createClass({ - render() { - return {this.state.message}; - } - }); +解释: - // Good - class Message extends PureComponent { - render() { - return {this.state.message}; - } - } - ``` +高阶组件事实上并非一个组件,而是一个“生成组件类型”的函数,因此遵守JavaScript函数命名的规范,使用camelCase命名。 -- [强制]不使用`state`的组件声明为函数组件。 +#### [建议] 使用`withXxx`或`xxxable`形式的词作为高阶组件的名称。 - 函数组件在React中有着特殊的地位,在将来也有可能得到更多的内部优化。 +解释: - ```javascript - // Bad - class NextNumber { - render() { - return {this.props.value + 1} - } +高阶组件是为组件添加行为和功能的函数,因此使用如上形式的词有助于对其功能进行理解。 + +## 3 组件声明 + +### 3.1 组件 + +#### [强制] 当需要类组件时,使用 ES Class 声明组件,禁止使用 `React.createClass` 。 + +解释: + +[React v15.5.0](https://facebook.github.io/react/blog/2017/04/07/react-v15.5.0.html)已经弃用了`React.createClass`函数。 + +示例: + +```js +// good +class Message extends PureComponent { + render() { + return {this.state.message}; } +} - // Good - let NextNumber = ({value}) => {value + 1}; - ``` +// bad +let Message = React.createClass({ + render() { + return {this.state.message}; + } +}); +``` -- [强制]所有组件均需声明`propTypes`。 +#### [强制] 尽量使用无状态函数组件。 - `propsTypes`在提升组件健壮性的同时,也是一种类似组件的文档的存在,有助于代码的阅读和理解。 +解释: -- [强制]对于所有非`isRequired`的属性,在`defaultProps`中声明对应的值。 +函数组件在React中有着特殊的地位,在将来也有可能得到更多的内部优化。 - 声明初始值有助于对组件初始状态的理解,也可以减少`propTypes`对类型进行校验产生的开销。 +示例: - 对于初始没有值的属性,应当声明初始值为`null`而非`undefined`。 +```js +// good +function NextNumber({value}) { + return {value + 1}; +} -- [强制]如无必要,使用静态属性语法声明`propsTypes`、`contextTypes`、`defaultProps`和`state`。 +// bad +class NextNumber { + render() { + return {this.props.value + 1} + } +} +``` - 仅当初始`state`需要从`props`计算得到的时候,才将`state`的声明放在构造函数中,其它情况下均使用静态属性声明进行。 +#### [建议] 无需显式引入 `React` 对象。 -- [强制]依照规定顺序编排组件中的方法和属性。 +解释: - 按照以下顺序编排组件中的方法和属性: +新版 `React` 使用 `react/jsx-runtime` 模块进行 JSX 元素处理,且由工具(如 Babel 等)负责引入,不需要显式引入。 - 1. `static displayName` - 2. `static propTypes` - 3. `static contextTypes` - 4. `state defaultProps` - 5. `static state` - 6. 其它静态的属性 - 7. 用于事件处理并且以属性的方式(`onClick = e => {...}`)声明的方法 - 8. 其它实例属性 - 9. `constructor` - 10. `getChildContext` - 11. `componentWillMount` - 12. `componentDidMount` - 13. `shouldComponentUpdate` - 14. `componentWillUpdate` - 15. `componentDidUpdate` - 16. `componentWillUnmount` - 17. 事件处理方法 - 18. 其它方法 - 19. `render` +对于其它功能,统一使用 Named Import 引入。 - 其中`shouldComponentUpdate`和`render`是一个组件最容易被阅读的函数,因此放在最下方有助于快速定位。 +示例: -- [建议]无需显式引入React对象。 +```js +import {useMemo, useCallback, memo} from 'react'; +``` - 使用JSX隐式地依赖当前环境下有`React`这一对象,但在源码上并没有显式使用,这种情况下添加`import React from 'react';`会造成一个没有使用的变量存在。 +#### [建议] 使用 `function` 关键字声明函数组件。 - 使用[babel-plugin-react-require](https://www.npmjs.com/package/babel-plugin-react-require)插件可以很好地解决这一问题,因此无需显式地编写`import React from 'react';`这一语句。 +解释: -- [建议]使用箭头函数声明函数组件。 +`function` 声明函数组件在类型推导上更加灵活,支持挂载子组件。且在与 `export default` 一起使用时,相比箭头函数依然可以保持函数名称。 - 箭头函数具备更简洁的语法(无需`function`关键字),且可以在仅有一个语句时省去`return`造成的额外缩进。 +### 3.2 属性 -- [建议]高阶组件返回新的组件类型时,添加`displayName`属性。 +#### [强制] 对于有默认值的可选属性,使用参数默认值声明对应的值,不使用 `defaultProps` 。 - 同时在`displayName`上声明高阶组件的存在。 +解释: - ```javascript - // Good - let asPureComponent = Component => { - let componentName = Component.displayName || Component.name || 'UnknownComponent'; - return class extends PureComponent { - static displayName = `asPure(${componentName})` +声明初始值有助于对组件的初始状态的理解,且使用参数的默认值声明有更好的性能,在 TypeScript 类型推导上也更准确。 - render() { - return ; - } - }; - }; - ``` +在值上,需要区分 `undefined` 与 `null` ,对于可选的属性,其默认值可能是 `null` ,此时依然需要通过参数默认值声明。 + +示例: -## 组件实现 +```jsx +// good +function Foo({value = 0}) { + return {value}; +} -- [强制]除顶层或路由级组件以外,所有组件均在概念上实现为纯组件(Pure Component)。 +// bad +function Foo({value}) { + return {value}; +} - 本条规则并非要求组件继承自`PureComponent`,“概念上的纯组件”的意思为一个组件在`props`和`state`没有变化(shallowEqual)的情况下,渲染的结果应保持一致,即`shouldComponentUpdate`应当返回`false`。 +Foo.defaultProps = { + value: 1, +}; +``` - 一个典型的非纯组件是使用了随机数或日期等函数: +### 3.3 生命周期 - ```javascript - let RandomNumber = () => {Math.random()}; - let Clock = () => {Date.time()}; - ``` +#### [强制] 禁止使用 `componentWillMount` 。 - 非纯组件具备向上的“传染性”,即一个包含非纯组件的组件也必须是非纯组件,依次沿组件树结构向上。由于非纯组件无法通过`shouldComponentUpdate`优化渲染性能且具备传染性,因此要避免在非顶层或路由组件中使用。 +解释: - 如果需要在组件树的某个节点使用随机数、日期等非纯的数据,应当由顶层组件生成这个值并通过`props`传递下来。对于使用Redux等应用状态管理的系统,可以在应用状态中存放相关值(如Redux使用Action Creator生成这些值并通过Action和reducer更新到store中)。 +使用 `constructor` 代替。 -- [强制]禁止为继承自`PureComponent`的组件编写`shouldComponentUpdate`实现。 +#### [强制] 禁止使用 `componentWillReceiveProps` 。 - 参考[React的相关Issue](https://github.com/facebook/react/issues/9239),在React的实现中,`PureComponent`并不直接实现`shouldComponentUpdate`,而是添加一个`isReactPureComponent`的标记,由`CompositeComponent`通过识别这个标记实现相关的逻辑。因此在`PureComponent`上自定义`shouldComponentUpdate`并无法享受`super.shouldComponentUpdate`的逻辑复用,也会使得这个继承关系失去意义。 +解释: -- [强制]为非继承自`PureComponent`的纯组件实现`shouldComponentUpdate`方法。 +对于类组件,使用 `getDerivedStateFromProps` 代替。 - `shouldComponentUpdate`方法在React的性能中扮演着至关重要的角色,纯组件必定能通过`props`和`state`的变化来决定是否进行渲染,因此如果组件为纯组件且不继承`shouldComponentUpdate`,则应当有自己的`shouldComponentUpdate`实现来减少不必要的渲染。 +#### [强制] 依照规定顺序编排组件中的方法和属性。 -- [建议]为函数组件添加`PureComponent`能力。 +按照以下顺序编排组件中的方法和属性: - 函数组件并非一定是纯组件,因此其`shouldComponentUpdate`的实现为`return true;`,这可能导致额外的无意义渲染,因此推荐使用高阶组件为其添加`shouldComponentUpdate`的相关逻辑。 +1. `static displayName` +2. `static propTypes` +3. `state defaultProps` +4. 其它静态的属性 +5. `state` +6. 其它实例属性 +7. 用于事件处理并且以属性的方式(`onClick = e => {...}`)声明的方法 +8. `constructor` +9. `componentDidMount` +10. `shouldComponentUpdate` +11. `static getDerivedStateFromProps` +12. `componentDidUpdate` +13. `componentWillUnmount` +14. 事件处理方法 +15. 其它方法 +16. `render` - 推荐使用[react-pure-stateless-component](https://www.npmjs.com/package/react-pure-stateless-component)库实现这一功能。 +### 3.4 高阶组件 -- [建议]使用`@autobind`进行事件处理方法与`this`的绑定。 +#### [建议] 高阶组件返回新的组件类型时,添加 `displayName` 属性。 - 由于`PureComponent`使用[`shallowEqual`](https://github.com/facebook/fbjs/blob/master/packages/fbjs/src/core/shallowEqual.js)进行是否渲染的判断,如果在JSX中使用`bind`或箭头函数绑定`this`会造成子组件每次获取的函数都是一个新的引用,这破坏了`shouldComponentUpdate`的逻辑,引入了无意义的重复渲染,因此需要在`render`调用之前就将事件处理方法与`this`绑定,在每次`render`调用中获取同样的引用。 +同时在 `displayName` 上声明高阶组件的存在。 - 当前比较流行的事前绑定`this`的方法有2种,其一使用类属性的语法: +```js +// good +let asPureComponent = Component => { + let componentName = Component.displayName || Component.name || 'UnknownComponent'; + return class extends PureComponent { + static displayName = `asPure(${componentName})` - ```javascript - class Foo { - onClick = e => { - // ... + render() { + return ; } }; - ``` +}; +``` - 其二使用`@autobind`的装饰器: +## 4 组件实现 - ```javascript - class Foo { - @autobind - onClick(e) { - // ... - } - } - ``` +### 4.1 更新机制 - 使用类属性语法虽然可以避免引入一个`autobind`的实现,但存在一定的缺陷: +#### [强制] 禁止为继承自 `PureComponent` 的组件编写 `shouldComponentUpdate` 实现。 - 1. 对于新手不容易理解函数内的`this`的定义。 - 2. 无法在函数上使用其它的装饰器(如`memoize`、`deprecated`或检验相关的逻辑等)。 +参考[React的相关Issue](https://github.com/facebook/react/issues/9239),在 React 的实现中, `PureComponent` 并不直接实现 `shouldComponentUpdate` ,而是添加一个 `isReactPureComponent` 的标记,由 `CompositeComponent` 通过识别这个标记实现相关的逻辑。因此在 `PureComponent` 上自定义 `shouldComponentUpdate` 并无法享受 `super.shouldComponentUpdate` 的逻辑复用,也会使得这个继承关系失去意义。 - 因此,推荐使用`@autobind`装饰器实现`this`的事先绑定,推荐使用[core-decorators](https://www.npmjs.com/package/core-decorators)库提供的相关装饰器实现。 +#### 【强制】不在组件中声明另一个组件。 -## JSX +解释: -- [强制]没有子节点的非DOM组件使用自闭合语法。 +在 React 的更新判断中,当一个元素(`Element`)的类型或 `key` 变化时,元素会被销毁并重建,所有状态丢失,且引起更大的 DOM 变化,进而影响性能。 - 对于DOM节点,按照HTML编码规范相关规则进行闭合,**其中void element使用自闭合语法**。 +在一个组件的渲染逻辑中创建另一个组件的类型,相当于每一次渲染都会有一个新的组件类型,即每次渲染都会引起销毁、重建逻辑,产生不必要的性能损耗和不预期的状态变化。 - ```javascript - // Bad - +因此,对于所谓的“子组件”,我们应当把它提到源代码的顶层,多个组件之间用 `props` 通信。 - // Good - - ``` +示例: -- [强制]保持起始和结束标签在同一层缩进。 +```jsx +// good +function Bar({name}) { + // ... +} - 对于标签前面有其它语句(如`return`的情况,使用括号进行换行和缩进)。 +function Foo({name}) { + return ; +} - ```javascript - // Bad - class Message { - render() { - return
    - Hello World -
    ; - } +// bad +function Foo({name}) { + function Bar() { + // ... } - // Good - class Message { - render() { - return ( -
    - Hello World -
    - ); - } - } - ``` + return ; +} +``` + +#### 【强制】为useMemo、useCallback、useEffect等有依赖的hook提供全部的依赖项。 + +解释: + +React 的 `useCallback`、`useMemo` 和 `useEffect` hook 有第2个参数表示依赖数组,当依赖数组中元素变化时,hook 对应的函数会被重新执行。这是符合 React 的响应式概念的,应当遵守。 + +你可以使用 [eslint-plugin-react-hooks](https://github.com/facebook/react/tree/main/packages/eslint-plugin-react-hooks) 中的 `react-hooks/exhaustive-deps` 规则进行检查,也可以自动修复依赖数组的内容。 + +如果你需要在特殊情况下需要省略部分依赖,你可以使用 `useRef` 保存该依赖的值。 + +#### 【强制】不得在 `setState` 的参数值中使用 `state` 对象。 + +解释: - 对于直接`return`的函数组件,可以直接使用括号而省去大括号和`return`关键字: +由于 `setState` 的异步特性,实际在更新状态发生时,可能 `state` 对象已经是过期的,因此需要使用回调的形式调用 `setState` 。 - ```javascript - let Message = () => ( +示例: + +```js +// good +let [counter, setCounter] = useState(0); +let increment = () => setCounter(c => c + 1); + +// bad +let [counter, setCounter] = useState(0); +let increment = () => setCounter(counter + 1); +``` + +#### 【建议】不要在 `didMount` 和 `didUpdate` 中使用 `setState` 。 + +解释: + +对于类组件, `didMount` 和 `didUpdate` 是发生在状态变化后的,此时再触发状态变更会引起多余的渲染,甚至可能引起死循环的无限渲染。 + +通常类似情况,就当将多次的 `setState` 合并成一个,或考虑状态的粒度是否正确。 + +在极少数情况下,如需要 DOM 元素来获取状态值,可以忽视本规则。 + +#### 【强制】不直接修改 `state` 和 `props`。 + +解释: + +React 的组件是由不可变的状态和属性驱动的,因此对 `state` 和 `props` 的直接修改并不会触发渲染和更新视图,会导致状态与界面的不一致。 + +在任何需要修改状态的场合,就当使用 `setState` 来更新,对于 `props` 则就当保持不可变的前提下,通过回调由父元素修改后传递新值到子组件。 + +### 4.2. 渲染逻辑 + +#### 【强制】不要在条件、循环语句中调用hook。 + +解释: + +Hooks 的本质是一个用数组保存的中间状态,它严格依赖各个 hook 调用的顺序和类型,在多次渲染中不能有任何变化。 + +在条件语句或循环语句中使用 hook ,会让组件保存的 hook 数组的数量和顺序产生变化,进而引起渲染的问题。 + +#### 【强制】不使用字符串类型的 `ref` 属性。 + +解释: + +在较新版本的 React 中,字符串类型的 `ref` 属性已经被废弃。 + +对于类组件,使用 `createRef` 函数创建 `ref` 对象。对于函数组件,使用 `useRef` 创建。 + +### 【建议】不使用 `dangerslySetInnerHTML` 。 + +解释: + +直接使用 `dangerslySetInnerHTML` 相当于使用 `innerHTML` ,存在XSS风险。如必须使用的场景,谨慎评估后使用注释说明原因。 + +#### 【建议】不使用 `findDOMNode` 。 + +解释: + +在较新版本的 React 中 `findDOMNode` 已经被废弃,就当使用 `ref` 来实现相应功能。 + +## 5 JSX + +### 5.1 标签 + +#### [强制] 没有子节点的组件使用自闭合语法。 + +解释: + +JSX与HTML不同,所有元素均可以自闭合。 + +示例: + +```js +// good + +
    + +// bad + +
    +``` + +#### [强制] 保持起始和结束标签在同一层缩进。 + +解释: + +对于标签前面有其它语句(如`return`的情况,使用括号进行换行和缩进)。 + +对于直接`return`的函数组件,可以直接使用括号而省去大括号和`return`关键字: + +```js +function Message() { + return (
    Hello World
    ); - ``` +} +``` -- [强制]对于多属性需要换行,从第一个属性开始,每个属性一行。 +示例: - ```javascript - // 没有子节点 - +```js +// good +class Message { + render() { + return ( +
    + Hello World +
    ; + ); + } +} + +// bad +class Message { + render() { + return
    + Hello World +
    ; + } +} +``` - // 有子节点 - - - - - ``` +#### 【强制】无子元素的DOM组件必须使用自闭合的形式书写。 -- [强制]以字符串字面量作为值的属性使用双引号(`"`),在其它类型表达式中的字符串使用单引号(`'`)。 +示例: - ```javascript - // Bad - - +```jsx +// good +function Foo() { + return
    ; +} - // Good - - - ``` +// bad +function Foo() { + return
    ; +} -- [强制]自闭合标签的`/>`前添加一个空格。 +#### [强制] 自闭合标签的`/>`前添加一个空格。 - ```javascript - // Bad - - +示例: - // Good - - ``` +```js +// bad + + -- [强制]对于值为`true`的属性,省去值部分。 +// good + +``` - ```javascript - // Bad - +### 5.2 属性 - // Good - - ``` +#### [强制] 对于多属性需要换行,从第一个属性开始,每个属性一行。 -- [强制]对于需要使用`key`的场合,提供一个唯一标识作为`key`属性的值,禁止使用可能会变化的属性(如索引)。 +示例: - `key`属性是React在进行列表更新时的重要属性,如该属性会发生变化,渲染的性能和**正确性**都无法得到保证。 +```js +// 没有子节点 + - ```javascript - // Bad - {list.map((item, index) => )} +// 有子节点 + + + + +``` - // Good - {list.map(item => )} - ``` +#### [强制] 以字符串字面量作为值的属性使用双引号(`"`),在其它类型表达式中的字符串使用单引号(`'`)。 -- [建议]避免在JSX的属性值中直接使用对象和函数表达式。 +示例: - `PureComponent`使用[`shallowEqual`](https://github.com/facebook/fbjs/blob/master/packages/fbjs/src/core/shallowEqual.js)对`props`和`state`进行比较来决定是否需要渲染,而在JSX的属性值中使用对象、函数表达式会造成每一次的对象引用不同,从而`shallowEqual`会返回`false`,导致不必要的渲染。 +```js +// good + + +// bad + + +``` - ```javascript - // Bad - class WarnButton { - alertMessage(message) { - alert(message); - } +#### [强制] 对于值为`true`的属性,省去值部分。 - render() { - return - } +示例: + +```js +// good + + +// bad + +``` + +#### [强制] 值为`true`的属性放在其它属性前面。 + +解释: + +将`true`类的值放在前面使得组件的声明更接近自然语言,提高可读性。 + +示例: + +```js +// good + + +// bad + +``` + +#### [强制] 对于需要使用`key`的场合,提供一个唯一标识作为`key`属性的值,禁止使用可能会变化的属性(如索引)。 + +解释: + +`key`属性是React在进行列表更新时的重要属性,如该属性会发生变化,渲染的性能和**正确性**都无法得到保证。 + +示例: + +```js +// good +{list.map(item => )} + +// bad +{list.map((item, index) => )} +``` + +#### [建议] 对于 `PureComponent` ,避免在JSX的属性值中直接使用对象和函数表达式。 + +解释: + +`PureComponent` 使用对 `props` 和 `state` 进行比较来决定是否需要渲染,而在JSX的属性值中使用对象、函数表达式会造成每一次的对象引用不同,从而 `shallowEqual` 会返回 `false` ,导致不必要的渲染。 + +示例: + +```js +// good +class WarnButton extends PureComponent { + alertMessage = () => { + alert(this.props.message); + }; + + render() { + return } +} - // Good - class WarnButton { - @autobind - alertMessage() { - alert(this.props.message); - } +// bad +class WarnButton extends PureComponent { + alertMessage(message) { + alert(message); + } - render() { - return - } + render() { + return } - ``` +} +``` -- [建议]将JSX的层级控制在3层以内。 +#### 5.2.1 层级 - JSX提供了基于组件的便携的复用形式,因此可以通过将结构中的一部分封装为一个函数组件来很好地拆分大型复杂的结构。层次过深的结构会带来过多缩进、可读性下降等缺点。如同控制函数内代码行数和分支层级一样,对JSX的层级进行控制可以有效提升代码的可维护性。 +#### [建议] 将JSX的层级控制在5层以内。 - ```javascript - // Bad - let List = ({items}) => ( -
      - { - items.map(item => ( -
    • -
      -

      {item.title}

      - {item.subtitle} -
      -
      {item.content}
      -
      - {item.author}@ -
      -
    • - )) - } -
    - ); +解释: + +JSX提供了基于组件的便携的复用形式,因此可以通过将结构中的一部分封装为一个函数组件来很好地拆分大型复杂的结构。层次过深的结构会带来过多缩进、可读性下降等缺点。如同控制函数内代码行数和分支层级一样,对JSX的层级进行控制可以有效提升代码的可维护性。 + +示例: - // Good - let Header = ({title, subtitle}) => ( +```js +// good +function Header({title, subtitle}) { + return (

    {title}

    {subtitle}
    ); +} - let Content = ({content}) =>
    {content}
    ; +function Content({content}) { + return
    {content}
    ; +} - let Footer = ({author, postTime}) => ( +function Footer({author, postTime}) { + return (
    {author}@
    ); +} - let Item = item => ( +function Itemitem() { + return (
    - ); + ) +} - let List = ({items}) => ( +function List({items}) { + return (
      {items.map(Item)}
    ); - ``` +} + +// bad +function List({items}) { + return ( +
      + { + items.map(item => ( +
    • +
      +

      {item.title}

      + {item.subtitle} +
      +
      {item.content}
      +
      + {item.author}@ +
      +
    • + )) + } +
    + ); +} +``` + +## 补充部分 + +以下为本次待新增的,暂不写具体解释,有异议的直接评论。 + +### 【强制】跨多行的自闭合组件元素,闭合符独立一行且与起始符对齐。 + +示例: + +```jsx +//good + + +//bad + + + +``` + +### 【强制】字符串类型的属性不能使用 `{}` 包裹。 + +``jsx +// good + + +// bad + +``` + +### 【强制】属性的=两侧不能有空格。 + +示例: + +```jsx +// good + + +// bad + +``` + +### 【强制】属性中的大括号两侧不能有空格。 + +示例: + +```jsx +// good + + +// bad + +``` + +### 【强制】如果属性值跨多行,属性的起始 `{ 后必须换行,结束 `}` 前必须换行。 + +示例: + +```jsx +// good + + 这是一个有 + {linesCount}行 + 组成的内容 + + } +/> + +// bad + + 这是一个有 + {linesCount}行 + 组成的内容 + } +/> +``` + +### 【强制】不得包含无用的Fragment元素。 + +解释: + +当一个 `Fragment` 不用于指定 `key` 属性,且不用于将多个元素聚为一组时,它是可以被删除的。 + +示例: + +```jsx +// good +<> + + + +<> {foo} + + <> +
    +
    + + +{item.value} + +// bad +<>{foo} +<> +

    <>foo

    +foo +
    + <> +
    +
    + +
    +``` + +### 【建议】回调类属性放在组件属性声明的最后。 + +解释: + +从组件的声明上,通过合适地排列属性,可以清晰地将它分为控制展示的“状态”和控制交互的“回调”,两者区分开来能够使表达更清晰。 + +同时,配合“回调属性均以 `onXxx` 形式命名”的规则,当每个属性一行时,对齐也会更一致。 + +```jsx +// good +
    + +// bad + +``` diff --git a/typescript.md b/typescript.md new file mode 100644 index 0000000..2b7d22a --- /dev/null +++ b/typescript.md @@ -0,0 +1,3370 @@ +# TypeScript编码规范 + +## 1 前言 + +随着 TypeScript 的不断发展,越来越多的开发者认可并使用 TypeScript 开发应用。本文档的目标是使 TypeScript 新特性的代码风格保持一致,并给予一些实践建议。 +本文档基本遵循 JavaScript Style Guide,并增设与 TypeScript 相关的规则。 + +## 2 规则 + +### [强制] 将重载函数、方法的声明放置在一起 + +解释: + +当一个函数或方法有多个类型的重载时,将它们放置在一起有助于代码阅读时很好地判断一个函数全部的重载方式。 + +你可以使用以下 ESLint 规则进行自动化检测: + +```json +{ + "@typescript-eslint/adjacent-overload-signatures": "error" +} +``` + +示例: + +```ts +// good +interface Foo { + foo(s: string): void; + foo(n: number): void; + foo(sn: string | number): void; + bar(): void; +} + +export function bar(): void; +export function foo(s: string): void; +export function foo(n: number): void; +export function foo(sn: string | number): void; + +// bad +interface Foo { + foo(s: string): void; + foo(n: number): void; + bar(): void; + foo(sn: string | number): void; +} + +export function foo(s: string): void; +export function foo(n: number): void; +export function bar(): void; +export function foo(sn: string | number): void; +``` + +### 【强制】 使用规范的数组类型定义形式 + +解释: + +数组在TypeScript中有 T[] 或者 Array 两种功能一致的定义形式,使用统一的一种定义形式可以帮助代码库更加易于阅读理解。 + +- T 为简单类型(原始类型或其引用),使用 T[] 定义数组。 +- T为联合类型、交叉类型、对象、函数时,使用 Array 定义数组。 + +- 只读类型统一使用 Array 定义数组。 + +你可以使用以下 ESLint 规则进行自动化检测: + +```json +{ + "@typescript-eslint/array-type": ["error", {"default": "array-simple", "readonly": "generic"}] +} +``` + +示例: + +```ts +// good +const a: Array = ['a', 'b']; +const b: Array<{ prop: string }> = [{ prop: 'a' }]; +const c: Array<() => void> = [() => {}]; +const d: MyType[] = ['a', 'b']; +const e: string[] = ['a', 'b']; + +const f: ReadonlyArray = ['a', 'b']; +const g: ReadonlyArray<{ prop: string }> = [{ prop: 'default' }]; + +// bad +const a: (string | number)[] = ['a', 'b']; +const b: { prop: string }[] = [{ prop: 'a' }]; +const c: (() => void)[] = [() => {}]; +const d: Array = ['a', 'b']; +const e: Array = ['a', 'b']; + +const f: readonly string[] = ['a', 'b']; +const g: readonly { prop: string }[] = [{ prop: 'default' }]; +``` + +### 【强制】禁止 await 非 thenable 的值 + +解释: + +thenable 指带有 then 方法的对象, 比如 Promise。有助于避免一些书写失误之类的错误。 + +你可以使用以下 ESLint 规则进行自动化检测: + +```json +{ + "@typescript-eslint/await-thenable": "error" +} +``` + +示例: + +```ts +// good +await Promise.resolve('value'); + +const createValue = async () => 'value'; +await createValue(); + +// bad +await 'value'; + +const createValue = () => 'value'; +await createValue(); +``` + +### 【强制】禁止通过命令注释强制解除ts的类型检查 + +解释: + +ts中可以使用@ts-的注释指令,用于指定单行或单个文件的 ts 类型检查规则。规定的命令注释使用规则如下: + +```ts +// @ts-expect-error // 下一行的代码类型错误为预期内的,无需报错(如果使用了但实际没错,反而报错)。允许使用但需要进行不少于3个字的说明。 +// @ts-ignore // 忽略下一行的类型检查错误,不允许使用。 +// @ts-nocheck // 整个文件关闭ts类型检查,不允许使用。 +// @ts-check // 整个文件开启ts类型检查,允许使用。 +``` + +你可以使用以下 ESLint 规则进行自动化检测: + +```json +{ + "@typescript-eslint/ban-ts-comment": [ + "error", + { + "ts-expect-eror": "allow-with-description", + "ts-ignore": true, + "ts-nocheck": true, + "ts-check": false, + "minimumDescriptionLength": 3 + } + ] +} +``` + +示例: + +```ts +// good +if (false) { + // Compiler warns about unreachable code error + console.log('hello'); +} + +// bad +if (false) { + // @ts-ignore: Unreachable code error + console.log('hello'); +} +if (false) { + /* + @ts-ignore: Unreachable code error + */ + console.log('hello'); +} +``` + +### 【强制】禁止通过命令注释强制解除tslint的检查 + +解释: + +不允许使用// tslint: 或 /* tslint: */的注释指令,解除单行或整个文件的 tslint 规范检查。 + +- / tslint: —— 代表注释下的一行,该规则生效。 +- /* tslint: */ —— 代表整个文件,该规则生效 。 + +你可以使用以下 ESLint 规则进行自动化检测: + +```json +{ + "@typescript-eslint/ban-tslint-comment": "error" +} +``` + +示例: + +```ts +// good + +// This is a comment that just happens to mention tslint +/* This is a multiline comment that just happens to mention tslint */ +someCode(); // This is a comment that just happens to mention tslint + +// bad + +/* tslint:disable */ +/* tslint:enable */ +/* tslint:disable:rule1 rule2 rule3... */ +/* tslint:enable:rule1 rule2 rule3... */ +// tslint:disable-next-line +someCode(); // tslint:disable-line +// tslint:disable-next-line:rule1 rule2 rule3... +``` + +### 【强制】禁用部分内置类型别名定义类型 + +解释: + +不允许使用String、Object、Boolean、Number、Symbol、BigInt、Function等内置类型别名进行类型定义,这是不安全的。 + +你可以使用以下 ESLint 规则进行自动化检测: + +```json +{ + "@typescript-eslint/ban-types": "error" +} +``` + +示例: + +```ts +// good +const str: string = 'foo'; +const bool: boolean = true; +const num: number = 1; +const symb: symbol = Symbol('foo'); +const bigInt: bigint = 1n; + +const func: () => number = () => 1; + +const lowerObj: object = {}; +const capitalObj: { a: string } = { a: 'string' }; + +const curly1: number = 1; +const curly2: Record<'a', string> = { a: 'string' }; + +// bad + +// use lower-case primitives for consistency +const str: String = 'foo'; +const bool: Boolean = true; +const num: Number = 1; + + + => 1; + +// use safer object types +const lowerObj: Object = {}; +const capitalObj: Object = { a: 'string' }; + +const curly1: {} = 1; +const curly2: {} = { a: 'string' }; +``` + +### 【强制】 把类的泛型参数写在类的构造器处 + +解释: + +ts中类的泛型参数有两种写法 『类型注解』或者 『构造函数注解』。请统一使用『构造函数注解』的写法, 有助于提升代码可读性。 + +```ts +const map: Map = new Map(); // 类型注解写法 +const map = new Map(); // 构造函数注解写法 +``` + +你可以使用以下 ESLint 规则进行自动化检测: + +```json +{ + "@typescript-eslint/consistent-generic-constructors": ["error", "constructor"] +} +``` + +示例: + +```ts +// good +const map = new Map(); +const map: Map = new MyMap(); +const set = new Set(); +const set = new Set(); +const set: Set = new Set(); + +// bad +const map: Map = new Map(); +const set: Set = new Set(); +``` + +### [推荐] 使用一致的风格定义任意键值的对象 + +解释: + +TS支持两种方式来定义任意键值的对象: + +1. 使用下标签名,如 + +```ts +interface Foo { + [key: string]: unknown; +} +type Foo = { + [key: string]: unknown; +}; +``` + +1. 使用内置类型Record,如 + +```ts +type Foo = Record; +``` + +这两种方式的作用是一样的,建议使用一致的风格。 + +你可以使用以下 ESLint 规则进行自动化检测: + +```json +{ + // 使用Record + "@typescript-eslint/consistent-indexed-object-style": ["warn", "record"] +} +``` + +示例: + +```ts +// good +type Foo = Record; + +// bad +interface Foo { + [key: string]: unknown; +} +type Foo = { + [key: string]: unknown; +}; +``` + +### [推荐] 使用一致的方式进行类型断言 + +解释: +TS提供两种类型断言的方式: + +1. 尖括号,如 value +2. as,如 value as Type + +建议使用一致的方式进行类型断言,以提高代码的可读性。 + +你可以使用以下 ESLint 规则进行自动化检测: + +```json +{ + // 变量作为参数使用时,可用as断言 + "@typescript-eslint/consistent-type-assertions": [ + "warn", + {"assertionStyle": "as", "objectLiteralTypeAssertions": "allow-as-parameter"} + ] +} +``` + +示例: + +```ts +// good +const x: T = { ... }; // const x: T = {...} 总是优于 const x = {...} as T +const y = { ... } as any; // 定义变量时,可以 as any +const z = { ... } as unknown; // 定义变量时,可以 as unknown +foo({ ... } as T); +new Clazz({ ... } as T); +function foo() { throw { bar: 5 } as Foo } +const foo = ; + +// bad +const x = { ... } as T; + +function foo() { + return { ... } as T; +} +``` + +### [推荐] 使用一致的方式进行类型定义 + +解释: + +TS允许使用type和interface两种方式来定义一个对象的类型,如 + +```ts +// type +type T1 = { + a: string; + b: number; +}; + +// interface +interface T2 { + a: string; + b: number; +} +``` + +这两种方式相近,但经常被混用。建议使用一致的方式进行类型定义,以提高代码的可读性。 + +你可以使用以下 ESLint 规则进行自动化检测: + +```json +{ + // 使用interface + "@typescript-eslint/consistent-type-definitions": ["warn", "interface"] +} +``` + +示例: + +```ts +// good +type T = string; // 非对象类型可用 type +type Foo = string | {}; // 混合类型可用 type + +interface T { + x: number; +} + +// bad +type T = { x: number }; +``` + +### [强制] 明确类成员的可访问性 + +解释: + +TS允许使用public、protected和private来修饰类成员的可访问性,默认值为public。 + +使用访问修饰符可以明确哪些属性和方法是类私有的,哪些是公共可访问的,有利于提高代码的可读性。 + +可选项: + +```ts +// explicit 需标明;no-public 不需要public;off 关闭检测 +type AccessibilityLevel = "explicit" | "no-public" | "off"; + +interface Options { + accessibility?: AccessibilityLevel; // 通用配置 + overrides?: { // 规则重载,特定类型的成员可使用以下配置覆盖accessibility + accessors?: AccessibilityLevel; // 访问器 get/set + constructors?: AccessibilityLevel; // 构造器 + methods?: AccessibilityLevel; // 方法 + properties?: AccessibilityLevel; // 属性 + parameterProperties?: AccessibilityLevel; // 参数属性 + }; + ignoredMethodNames?: string[]; // 指定忽略检测的方法 +} + +// 默认需标明所有类成员的可访问性 +const defaultOptions: Options = [{ accessibility: "explicit" }]; +``` + +你可以使用以下 ESLint 规则进行自动化检测: + +```json +{ + // 禁用public(默认为public,不需要额外去写) + "@typescript-eslint/explicit-member-accessibility": ["error", {"accessibility": "no-public"}] +} +``` + +示例: + +```ts +// good +class Animal { + constructor(protected breed, name) { + // Parameter property and constructor + this.name = name; + } + private animalName: string; // Property + get name(): string { + // get accessor + return this.animalName; + } + private set name(value: string) { + // set accessor + this.animalName = value; + } + protected walk() { + // method + } +} + +// bad +class Animal { + public constructor(public breed, name) { + // Parameter property and constructor + this.animalName = name; + } + public animalName: string; // Property + public get name(): string { + // get accessor + return this.animalName; + } + public set name(value: string) { + // set accessor + this.animalName = value; + } + public walk() { + // method + } +} +``` + +### [强制] 使用一致的风格分隔对象类型定义的成员 + +解释: + +官方:使用Linter检测不如使用Formatter自动格式化。 + +TS提供三种方式来分隔interface或type定义的对象类型的成员,如 + +```ts +interface Foo { + // 分号(semi),默认值,推荐使用 + name: string; + // 逗号(comma),JSON风格 + name: string, + // 换行(none) + name: string +} +``` + +应使用一致的分隔符,以提高代码的可读性。 + +可选项: + +```ts +interface Options { + multiline?: { // 多行 + delimiter?: "none" | "semi" | "comma"; // 分隔符,换行|分号|逗号 + requireLast?: boolean; // 最后的成员后面是否需要分隔符 + }; + singleline?: { // 单行 + delimiter?: "semi" | "comma"; // 分隔符,分号|逗号 + requireLast?: boolean; + }; + overrides?: { + interface?: { // 针对interface定义的对象类型 + multiline?: { + delimiter?: "none" | "semi" | "comma"; + requireLast?: boolean; + }; + singleline?: { + delimiter?: "semi" | "comma"; + requireLast?: boolean; + }; + }; + typeLiteral?: { // 针对type定义的对象类型 + multiline?: { + delimiter?: "none" | "semi" | "comma"; + requireLast?: boolean; + }; + singleline?: { + delimiter?: "semi" | "comma"; + requireLast?: boolean; + }; + }; + }; + /** + * multilineDetection: 多行的定义方式 + * brackets: 默认值,在interface和type定义中出现任意换行,即为多行 + * last-member: 若最后一个成员和最后的括号在同一行,为单行 + */ + multilineDetection?: "brackets" | "last-member"; // +} + +// 默认值 +const defaultOptions: Options = [ + { + multiline: { delimiter: "semi", requireLast: true }, + singleline: { delimiter: "semi", requireLast: false }, + multilineDetection: "brackets", + }, +]; +``` + +你可以使用以下 ESLint 规则进行自动化检测: + +```json +{ + // 多行,分号分隔,最后添加;单行,逗号分隔,最后省略 + "@typescript-eslint/member-delimiter-style": ["error", {"multiline": "semi", "singleline": "comma"}] +} +``` + +示例: + +```ts +// good +// 多行,分号分隔,最后添加 +interface Foo { + name: string; + greet(): string; +} +type Bar = { + name: string; + greet(): string; +} + +// 单行,逗号分隔,最后省略 +interface Foo { name: string } +type Bar = { name: string } +type FooBar = { name: string, greet(): string } + +// bad +// 多行,用错或缺少分隔符 +interface Foo { + name: string + greet(): string +} +interface Bar { + name: string, + greet(): string, +} +interface Baz { + name: string; + greet(): string +} + +// 单行,用错或多余分隔符 +type FooBar = { name: string; greet(): string } +type FooBar = { name: string, greet(): string, } +``` + +### [推荐] 使用一致的对象成员顺序 + +解释: + +在使用class、interface和type定义一个对象的结构时,将属性、方法和构造器以一致的顺序进行排列可以使代码更容易阅读、检索和编辑。 + +你可以使用以下 ESLint 规则进行自动化检测: + +```json +{ + // [..., "signature", ..., "field", ..., "constructor", ..., "method", ...] + "@typescript-eslint/member-ordering": "warn" +} +``` + +示例: + +```ts +// good +class Foo { + [Z: string]: any; // -> signature + private C: string; // -> field + public D: string; // -> field + protected static E: string; // -> field + + constructor() {} // -> constructor + + public static A(): void {} // -> method + public B(): void {} // -> method +} +interface Foo { + [Z: string]: any; // -> signature + + B: string; // -> field + + new (); // -> constructor + + A(): void; // -> method +} +type Foo = { + // no signature + + B: string; // -> field + + // no constructor + + A(): void; // -> method +}; + +// bad +class Foo { + public static A(): void {} // -> method + public B(): void {} // -> method + + constructor() {} // -> constructor + + [Z: string]: any; // -> signature + + private C: string; // -> field + public D: string; // -> field + protected static E: string; // -> field +} +interface Foo { + A(): void; // -> method + + new (); // -> constructor + + B: string; // -> field + + [Z: string]: any; // -> signature +} +type Foo = { + A(): void; // -> method + + B: string; // -> field + + // no constructor + // no signature +}; +``` + +### [强制] 命名时必须匹配命名规范 + +解释: + +命名类型时匹配规范的拼写类型,有利于代码类型进行整理规范。不同类型可以进行一目了然的判断。 + +camelCase:驼峰拼写法。使用大写字符分割单词,单词之间不允许下划线,允许连续大写(例:myID) + +PascalCase:大驼峰。与camelCase类似,但第一个字符必须大写。(例:MyId) + +UPPER_CASE:字符必须大写,并且使用下划线分割单词。(例:MY_ID) + +注:均不允许以下划线开头或者结尾。 + +默认拼写格式:camelCase + +特例如下: + +variableLike( function, parameter, variable): 使用camelCase或者UPPER_CASE + +typelike( class, enum, interface, typeAlias, typeParameter):使用PascalCase + +enumMember:使用PascalCase或者UPPER_CASE + +你可以使用以下 ESLint 规则进行自动化检测: + +```json +{ + "@typescript-eslint/naming-convention": [ + "error", + [ + { + "selector": "default", + "format": ["camelCase"], + "leadingUnderscore": "forbid", + "trailingUnderscore": "forbid" + }, + { + "selector": "variableLike", + "format": ["camelCase", "UPPER_CASE"], + "leadingUnderscore": "forbid", + "trailingUnderscore": "forbid" + }, + { + "selector": "memberLike", + "format": ["camelCase"], + "leadingUnderscore": "forbid", + "trailingUnderscore": "forbid" + }, + { + "selector": "property", + "format": ["camelCase"], + "leadingUnderscore": "forbid", + "trailingUnderscore": "forbid" + }, + { + "selector": "method", + "format": ["camelCase"], + "leadingUnderscore": "forbid", + "trailingUnderscore": "forbid" + }, + { + "selector": "typeLike", + "format": ["PascalCase"], + "leadingUnderscore": "forbid", + "trailingUnderscore": "forbid" + }, + { + "selector": "enumMember", + "format": ["PascalCase", "UPPER_CASE"], + "leadingUnderscore": "forbid", + "trailingUnderscore": "forbid" + } + ] + ] +} +``` + +示例: + +```ts +// good +let modalVisible: boolean = false; +const PERSON_NAMES: string[] = ['张三', '李四']; + +function doSomething(variable: T) { + // do something + return variable; +} + +interface OptionItem { + label: string; + value: string; +} + +enum FOUR_DIRECTION{ // or FourDirection + Up, + Down, + Left, + Right, +} + +// bad +let modalvisible: boolean = false; +const PERSONNAMES: string[] = ['张三', '李四']; + +function dosomthing(Variable: t) { + // do something + return variable; +} + +interface optionItem { + Label: string; + Value: string; +} + +enum fourDirection { + up, + down, + left, + right, +} +``` + +### [建议] toString方法只在字符串化时提供有用信息的对象上调用 + +直接调用对象的toString方法,返回的往往不是一个期待的有用值。而是一个无用值 " [object Object] "。 + +所以要避免未定义toString未返回有用值时调用此方法。 + +解释: + +对象的toString默认方法返回 " [object Object] "。 + +若未定义toString方法,当使用 + 或者 `${}`进行转化时将调用toString方法。将得到一个无用值。 + +注:函数本身还有可用toString方法。 + +你可以使用以下 ESLint 规则进行自动化检测: + +```json +{ + "@typescript-eslint/no-base-to-string": "warn" +} +``` + +示例: + +```ts +// good + +// 原类型的toSring方法可用 +'Text:' + true; // Boolean + +`Value: ${123}`; // Number + +`Arrays: ${[1, 2, 3]}`; // Array + +(() => { }).toString(); // Function + +// 在对象/类内定义一个可用的toString方法 +const objectToString = { + toString: () => 'Hello, world!', +}; + +`Object: ${objectToString}`; + +class ObjectToString { + toString() { + return 'Hello, world!'; + } +} +`Class: ${new ObjectToString()}`; + +// bad +//直接调用 +'' + {}; +{}.toString() + +// 未定义toString方法调用 +class ObjectNoToString { } +new ObjectNoToString() + ''; +``` + +### [强制] 使用 === 判断时,不使用非空断言或者使用括号包裹 + +解释: + +当同时使用非空断言 ! 与 === 进行运算时,容易和 !== 进行混淆。 + +适当忽略单变量非必要非空断言,以及组合值使用括号包裹进行区分。 + +你可以使用以下 ESLint 规则进行自动化检测: + +```json +{ + "@typescript-eslint/no-confusing-non-null-assertion": "error" +} +``` + +示例: + +```ts +// good +interface Foo { + bar?: string; + num?: number; +} + +const foo: Foo = {}; + +const isEqualsBar = foo.bar == 'hello'; +const isEqualsNum = (1 + foo.num!) == 2; + +// bad +const isEqualsBar = foo.bar! == 'hello'; +const isEqualsNum = 1 + foo.num! == 2; +``` + +### [强制] 要求void类型的表达式出现在语句位置 + +解释: + +返回非必要值会造成函数有返回值的错觉。导致代码逻辑混乱。可以适当使用逻辑运算符进行避免。 + +你可以使用以下 ESLint 规则进行自动化检测: + +```json +{ + "@typescript-eslint/no-confusing-void-expression": "error" +} +``` + +示例: + +```ts +// good +alert('Hello, world!'); + +promise.then(value => { + window.postMessage(value); +}); + +function doSomething() { + if (!somethingToDo) { + console.error('Nothing to do!'); + return; + } + + console.log('Doing a thing...'); +} + +// 使用逻辑函数优化 +cond && console.log('true'); +cond || console.error('false'); +cond ? console.log('true') : console.error('false'); + +// bad +// 忘记某些函数无返回值 +const response = alert('Are you sure?'); + +// promise链中return非响应值 +promise.then(value => window.postMessage(value)); + +// 给予函数返回值的错觉 +function doSomething() { + if (!somethingToDo) { + return console.error('Nothing to do!'); + } + + console.log('Doing a thing...'); +} +``` + +### [强制] 禁止重复的enum成员值 + +解释: + +在一个枚举类型中,通常期望成员中具有唯一的值。重复的值会导致难以追踪的错误。 + +你可以使用以下 ESLint 规则进行自动化检测: + +```json +{ + "@typescript-eslint/no-duplicate-enum-values": "error" +} +``` + +示例: + +```ts +// good +enum E { + A = 0, + B = 1, +} + +enum E { + A = 'A', + B = 'B', +} + +// bad +enum E { + A = 0, + B = 0, +} + +enum E { + A = 'A', + B = 'A', +} +``` + +### [建议] 禁止对计算键表达式使用delete运算符 + +解释: + +错误的常量可能获取错误的数据结构。但使用变量/计算值可能会偶尔导致边缘性错误,比如键名为“hasOwnProperty”。 + +如果要存储对象的集合,可以考虑使用Map或Set。 + +你可以使用以下 ESLint 规则进行自动化检测: + +```json +{ + "@typescript-eslint/no-dynamic-delete": "warn" +} +``` + +示例: + +```ts +// good +const container: { [i: string]: number } = { + /* ... */ +}; + +delete container.aaa; +delete container[7]; +delete container['-Infinity']; + +// bad +delete container['aaa']; +delete container['Infinity']; + +const name = 'name'; +delete container[name]; +delete container[name.toUpperCase()]; +``` + +### [推荐] 不允许使用any类型 @佘采华 + +解释: +TypeScript 中的 any 类型是类型系统的一个危险的“escape hatch”。使用 any 会禁用许多类型检查规则,通常最好仅作为最后的手段或在制作原型代码时使用。 + +你可以使用以下 ESLint 规则进行自动化检测: + +```json +{ + "@typescript-eslint/no-explicit-any": "warn" +} +``` + +示例: + +```ts +// good +const age: number = 17; +const ages: number[] = [17]; +function greet(): string {} +function greet(): string[] {} + +// bad +const age: any = 'seventeen'; +const ages: any[] = ['seventeen']; +function greet(): any {} +function greet(): any[] {} +``` + +### [强制] 禁止额外的非空断言 @佘采华 + +解释: + +【!】 TypeScript 中的非空断言运算符用于断言值的类型不包括 null 或 undefined。对单个值多次使用该运算符没有任何作用。 + +你可以使用以下 ESLint 规则进行自动化检测: + +```json +{ + "@typescript-eslint/no-extra-non-null-assertion": "error" +} +``` + +示例: + +```ts +// good +const foo: { bar: number } | null = null; +const bar = foo!.bar; + +function foo(bar: number | undefined) { + const bar: number = bar!; +} +function foo(bar?: { n: number }) { + return bar?.n; +} + +// bad +const foo: { bar: number } | null = null; +const bar = foo!!!.bar; + +function foo(bar: number | undefined) { + const bar: number = bar!!!; +} + +function foo(bar?: { n: number }) { + return bar!?.n; +} +``` + +### [推荐] 不允许将类用作名称空间 @佘采华 + +解释: + +此规则报告类何时没有非静态成员,例如专门用作静态名称空间的类。 + +来自 OOP 范式的用户可能会将他们的实用函数包装在一个额外的类中,而不是将它们放在 ECMAScript 模块的顶层。在 JavaScript 和 TypeScript 项目中通常不需要这样做。 + +- - 包装类在没有添加任何结构改进的情况下为代码增加了额外的认知复杂性 + +1. 1. 1. 无论放在它们上面的是什么,都已经在模块中组织好了,例如有作用的函数。 + 2. 作为替代方案,您可以 import * as ... 模块以将所有这些都放在一个对象中导出。 + +- - 当你开始键入属性名称时,IDE 无法为静态类或命名空间导入的属性提供好的建议。 + - 当它们都在类中时,静态分析未使用变量等的代码会更加困难(see: [无效代码 (无效类型) in TypeScript](https://effectivetypescript.com/2020/10/20/tsprune)) + +XXX + +你可以使用以下 ESLint 规则进行自动化检测: + +```json +{ + "@typescript-eslint/no-extraneous-class": "warn" +} +``` + +示例: + +```ts +// good +export const version = 42; + +export function isProduction() { + return process.env.NODE_ENV === 'production'; +} + +function logHelloWorld() { + console.log('Hello, world!'); +} + +// bad +class StaticConstants { + static readonly version = 42; + + static isProduction() { + return process.env.NODE_ENV === 'production'; + } +} + +class HelloWorldLogger { + constructor() { + console.log('Hello, world!'); + } +} +``` + +### 【强制】要求适当处理类似 Promise 的语句 @佘采华 + +解释: + +“漂浮”Promise 是在没有设置任何代码来处理它可能抛出的任何错误的情况下创建的。漂浮 Promise 会导致几个问题,例如操作顺序不正确、忽略 Promise reject等。 + +- - 当 Promise 创建但未正确处理时,此规则会报告。处理 Promise 值语句的有效方法包括: + - await Promise + - return Promise + - .then() 两个参数处理 + - .catch()一个参数处理 + +你可以使用以下 ESLint 规则进行自动化检测: + +```json +{ + "@typescript-eslint/no-floating-promises": "warn" +} +``` + +示例: + +```ts +// good +const promise = new Promise((resolve, reject) => resolve('value')); +await promise; + +async function returnsPromise() { + return 'value'; +} +returnsPromise().then( + () => {}, + () => {}, +); + +Promise.reject('value').catch(() => {}); + +Promise.reject('value').finally(() => {}); + +// bad +const promise = new Promise((resolve, reject) => resolve('value')); +promise; + +async function returnsPromise() { + return 'value'; +} +returnsPromise().then(() => {}); + +Promise.reject('value').catch(); + +Promise.reject('value').finally(); +``` + +### 【强制】禁止使用 for-in 循环遍历数组。 @佘采华 + +解释: + +for-in 循环 (for (var i in o)) 遍历对象的属性。虽然对数组类型使用 for-in 循环是合法的,但并不常见。 for-in 将迭代数组的索引作为字符串,省略数组中存在的“漏洞”。 + +你可以使用以下 ESLint 规则进行自动化检测: + +```json +{ + "@typescript-eslint/no-for-in-array": "error" +} +``` + +示例: + +```ts +// good +declare const array: string[]; + +for (const value of array) { + console.log(value); +} + +for (let i = 0; i < array.length; i += 1) { + console.log(i, array[i]); +} + +array.forEach((value, i) => { + console.log(i, value); +}) + +for (const [i, value] of array.entries()) { + console.log(i, value); +} + +// bad +declare const array: string[]; + +for (const i in array) { + console.log(array[i]); +} + +for (const i in array) { + console.log(i, array[i]); +} +``` + +### 【强制】不允许对初始化为数字、字符串或布尔值的变量或参数进行显式类型声明。 @佘采华 + +解释: + +TypeScript 能够从参数、属性和变量的默认值或初始值推断出它们的类型。无需在初始化为布尔值、数字或字符串的那些构造之一上使用显式 **[: ]**类型注释。这样做会给代码增加不必要的冗长 - 使其更难阅读 - 在某些情况下可以防止 TypeScript 推断更具体的文字类型(eg: 10)而不是更通用的原始类型(eg: number)。 + +你可以使用以下 ESLint 规则进行自动化检测: + +```json +{ + "@typescript-eslint/no-inferrable-types": "error" +} +``` + +示例: + +```ts +// good +const a = 10n; +const a = BigInt(10); +const a = !0; +const a = Boolean(null); +const a = true; +const a = null; +const a = 10; +const a = Infinity; +const a = NaN; +const a = Number('1'); +const a = /a/; +const a = new RegExp('a'); +const a = `str`; +const a = String(1); +const a = Symbol('a'); +const a = undefined; +const a = void someValue; + +class Foo { + prop = 5; +} + +function fn(a = 5, b = true) {} + +// bad +const a: bigint = 10n; +const a: bigint = BigInt(10); +const a: boolean = !0; +const a: boolean = Boolean(null); +const a: boolean = true; +const a: null = null; +const a: number = 10; +const a: number = Infinity; +const a: number = NaN; +const a: number = Number('1'); +const a: RegExp = /a/; +const a: RegExp = new RegExp('a'); +const a: string = `str`; +const a: string = String(1); +const a: symbol = Symbol('a'); +const a: undefined = undefined; +const a: undefined = void someValue; + +class Foo { + prop: number = 5; +} + +function fn(a: number = 5, b: boolean = true) {} +``` + +### [强制] 不允许泛型或返回类型之外的void类型。 + +解释: + +void 指的是一个要忽略的函数返回。试图在返回类型或泛型类型参数之外使用void类型通常是程序员犯错的迹象,即使使用正确,void也会误导其他开发人员。 + +void类型意味着不能与任何其他类型混合,除了 never 类型,因为后者接受所有类型。如果你认为需要这样做,那么你可能需要使用 undefined 类型。 + +你可以使用以下 ESLint 规则进行自动化检测: + +```json +{ + "@typescript-eslint/no-invalid-void-type": "error" +} +``` + +示例: + +```ts +// good +type NoOp = () => void; + +function noop(): void {} + +let trulyUndefined = void 0; + +async function promiseMeSomething(): Promise {} + +type stillVoid = void | never; + +// bad +type PossibleValues = string | number | void; +type MorePossibleValues = string | ((number & any) | (string | void)); + +function logSomething(thing: void) {} +function printArg(arg: T) {} + +logAndReturn(undefined); + +interface Interface { + lambda: () => void; + prop: void; +} + +class MyClass { + private readonly propName: void; +} +``` + +### [强制] 不允许使用 void 运算符,除非用于丢弃值。 + +解释: + +void 指的是一个要忽略的函数返回。void 运算符是传达程序员丢弃值这一意图的有效工具。 + +该规则有助于捕获 API 更改,之前在调用站点丢弃了一个值,但被调用者已更改,因此不再返回值。 当与 no-unused-expressions 结合使用时,它还可以通过确保一致性来帮助代码的读者,如:类似 void foo(); 的语句, 总是丢弃一个返回值;类似 foo() 的语句, 永远不会丢弃返回值。 该规则报告参数已为 void 或undefined 类型的任何 void 运算符。 + +你可以使用以下 ESLint 规则进行自动化检测: + +```json +{ + "@typescript-eslint/no-meaningless-void-operator": "error" +} +``` + +示例: + +```ts +// good +(() => {})(); + +function foo() {} +foo(); // nothing to discard + +function bar(x: number) { + void x; // discarding a number + return 2; +} +void bar(); // discarding a number + +// bad +void (() => {})(); + +function foo() {} +void foo(); +``` + +### [强制] 强制执行 new 和构造函数的有效定义。 + +解释: + +JavaScript 类可以定义一个构造方法,该方法在新创建类实例时运行。 TypeScript 允许描述静态类对象的接口定义 new() 方法(尽管这在现实世界的代码中很少使用)。 刚接触 JavaScript 类或 TypeScript 接口的开发人员有时可能会混淆何时使用构造函数或 new。 + +当一个类定义了一个名为 new 的方法或一个接口定义了一个名为 constructor 的方法时,该规则会报告。 + +你可以使用以下 ESLint 规则进行自动化检测: + +```json +{ + "@typescript-eslint/no-misused-new": "error" +} +``` + +示例: + +```ts +// good +declare class C { + constructor(); +} + +interface I { + new (): C; +} + +// bad +declare class C { + new(): C; +} + +interface I { + new (): I; + constructor(): void; +} +``` + +### [强制] 不允许在不适合使用 Promises 的地方使用它们。 + +解释: + +该规则禁止在 TypeScript 编译器允许但不适合的地方使用 Promise 来进行逻辑运算,例如 if 语句。 这些情况通常是由于缺少 await 关键字或只是误解了处理/等待异步函数的使用方式而引起的。 + +no-misused-promises 仅检测向不正确的逻辑位置提供 Promise 的代码。 请参阅 no-floating-promises 以检测未处理的 Promise 语句。 + +你可以使用以下 ESLint 规则进行自动化检测: + +```json +{ + "@typescript-eslint/no-misused-promises": "error" +} +``` + +示例: + +```ts +// good +const promise = Promise.resolve('value'); + +// Always `await` the Promise in a conditional +if (await promise) { + // Do something +} + +const val = (await promise) ? 123 : 456; + +while (await promise) { + // Do something +} + + +// bad +const promise = Promise.resolve('value'); + +if (promise) { + // Do something +} + +const val = promise ? 123 : 456; + +while (promise) { + // Do something +} +``` + +### [强制] 禁止命名空间 namespace + +解释: + +TypeScript 曾经允许一种称为“自定义模块”( module Example {} )的代码组织形式,后来被重命名为“命名空间”(namespace Example)。命名空间是组织TypeScript 代码的过时方法。现在首选ES2015模块语法( import/export)。 + +此规则不会报告使用 TypeScript 模块声明来描述外部 API(declare module 'foo' {})。 + +你可以使用以下 ESLint 规则进行自动化检测: + +```json +{ + "@typescript-eslint/no-namespace": "error" +} +``` + +示例: + +```ts +// good +declare module 'foo' {} + +// anything inside a d.ts file + +// bad +module foo {} +namespace foo {} + +declare module foo {} +declare namespace foo {} +``` + +### [强制] 不允许在空合并运算符的左操作数中使用非空断言 + +解释: + +?? 运算符允许在处理 null 或 undefined 时提供默认值。 在零合并运算符的左侧使用 ! 运算符是多余的,并且可能标志着程序员错误或混淆了这两个运算符。 + +你可以使用以下 ESLint 规则进行自动化检测: + +```json +{ + "@typescript-eslint/no-non-null-asserted-nullish-coalescing": "error" +} +``` + +示例: + +```ts +// good +foo ?? bar; +foo ?? bar!; +foo!.bazz ?? bar; +foo!.bazz ?? bar!; +foo() ?? bar; + +// This is considered correct code because there's no way for the user to satisfy it. +let x: string; +x! ?? ''; + +// bad +foo! ?? bar; +foo.bazz! ?? bar; +foo!.bazz! ?? bar; +foo()! ?? bar; + +let x!: string; +x! ?? ''; + +let x: string; +x = foo(); +x! ?? ''; +``` + +### [推荐] 不允许非空断言与可选链同时使用 + +解释: + +对可选链提供的可能为空的值进行非空断言很可能是错误的。 + +你可以使用以下 ESLint 规则进行自动化检测: + +```json +{ + "typescript-eslint/no-non-null-asserted-optional-chain": "warn" +} +``` + +示例: + +```ts +// good +foo?.bar; +foo?.bar(); + +// bad +foo?.bar!; +foo?.bar()!; +``` + +### [推荐] 不使用非空断言 + +解释: + +使用断言来绕过编译的非空判断,通常意味着代码本身的类型安全可能存在隐患。通常更好的做法是重构代码逻辑,使Typescript能够理解什么时候值可能是空的。 + +你可以使用以下 ESLint 规则进行自动化检测: + +```json +{ + "@typescript-eslint/no-non-null-assertion": "warn" +} +``` + +示例: + +```ts +interface Example { + property?: string; +} +declare const foo: Example; + +// good +const includesBaz = foo.property?.includes('baz') ?? false; + +// bad +const includesBaz = foo.property!.includes('baz'); +``` + +### [强制] 不允许冗余的类型成员 + +解释: + +在联合类型( | )或者交叉类型( & )中,一些类型成员可以覆盖其他类型成员,或者被其他类型成员所覆盖。在以下规则中,部分的类型成员在联合类型/交叉类型中会不起作用。 + +联合类型中: + +- - any 和 unknown 覆盖其他所有联合类型成员 + - never 会在联合类型中不起作用,除非是在返回值类型中 + - 基本数据类型会覆盖其任何一个字面量类型,例如string会覆盖'' + +交叉类型中: + +- - any 和 never 覆盖其他所有交叉类型成员 + - unknown 在交叉类型中不起作用 + - 字面量类型覆盖所有基本数据类型 + - 字面量类型例如""覆盖其对应的任何基本数据类型,例如string + +你可以使用以下 ESLint 规则进行自动化检测: + +```json +{ + "@typescript-eslint/no-redundant-type-constituents": "error" +} +``` + +示例: + +```ts +// good +type UnionAny = any; +type UnionUnknown = unknown; +type UnionNever = never; + +type UnionBooleanLiteral = boolean; +type UnionNumberLiteral = number; +type UnionStringLiteral = string; + +type IntersectionAny = any; +type IntersectionUnknown = string; +type IntersectionNever = string; + +type IntersectionBooleanLiteral = false; +type IntersectionNumberLiteral = 1; +type IntersectionStringLiteral = 'foo'; + +// bad +type UnionAny = any | 'foo'; +type UnionUnknown = unknown | 'foo'; +type UnionNever = never | 'foo'; + +type UnionBooleanLiteral = boolean | false; +type UnionNumberLiteral = number | 1; +type UnionStringLiteral = string | 'foo'; + +type IntersectionAny = any & 'foo'; +type IntersectionUnknown = string & unknown; +type IntersectionNever = string | never; + +type IntersectionBooleanLiteral = boolean & false; +type IntersectionNumberLiteral = number & 1; +type IntersectionStringLiteral = string & 'foo'; +``` + +### [强制] 不允许使用require引入模块 + +解释: + +请使用ES6的import代替require。 + +你可以使用以下 ESLint 规则进行自动化检测: + +```json +{ + "@typescript-eslint/no-require-imports": "error" +} +``` + +示例: + +```ts +// good +import * as lib1 from 'lib1'; +import { lib2 } from 'lib2'; +import * as lib3 from 'lib3'; + +// bad +const lib1 = require('lib1'); +const { lib2 } = require('lib2'); +import lib3 = require('lib3'); +``` + +### [推荐] 不为this设置别名 + +解释: + +将this赋值给一个变量而非直接使用箭头函数,如果不是在非ES6的旧项目中,就需要考虑是否未能合理地管理好作用域。 + +你可以使用以下 ESLint 规则进行自动化检测: + +```json +{ + "@typescript-eslint/no-this-alias": "warn" +} +``` + +示例: + +```ts +// good +setTimeout(() => { + this.doWork(); +}); + +// bad +const self = this; + +setTimeout(function () { + self.doWork(); +}); +``` + +### [推荐] 不使用不必要的布尔字面量进行比较 + +解释: + +当一个变量只可能是布尔类型时,直接使用变量的值本身或其一元否定进行判断更加简洁明了。而当变量的类型是包含布尔值的联合类型时,用布尔字面量进行对比才是有意义的。 + +你可以使用以下 ESLint 规则进行自动化检测: + +```json +{ + "@typescript-eslint/no-unnecessary-boolean-literal-compare": "warn" +} +``` + +示例: + +(注:尽管示例中均使用了严格相等===,但是实际上宽松相等==也是如此) + +```ts +// good +declare const someCondition: boolean; +if (someCondition) { +} + +declare const someObjectBoolean: boolean | Record; +if (someObjectBoolean === true) { +} + +declare const someStringBoolean: boolean | string; +if (someStringBoolean === true) { +} + +// bad +declare const someCondition: boolean; +if (someCondition === true) { +} +``` + +### [推荐]避免不必要的条件判断 + +解释: + +当一个表达式被用于条件判断时,如果已经能根据表达式的类型推断出表达式的计算结果为真或者为假,那么这个条件判断是不必要的。涉及这一规则的场景有: + +- `&&`,`||`和`?:`(三元)运算符的参数 +- `if`, `for`, `while`, 和`do-while`语句的条件 +- 可选链表达式的基值 + +你可以使用以下 ESLint 规则进行自动化检测: + +```json +{ + "@typescript-eslint/no-unnecessary-condition": "warn" +} +``` + +示例: + +```ts +// good +function head(items: T[]) { + // items有可能是个空数组,所以这里的判断是有必要的 + if (items.length) { + return items[0].toUpperCase(); + } +} + +function foo(arg: string) { + // arg有可能是空字符串,所以这里的判断是有必要的 + if (arg) { + } +} + +// bad +function foo(arg: 'bar' | 'baz') { + // arg 不会为null、undefined或者空字符串,所以这个if条件判断是不必要的 + if (arg) { + } +} + +function bar(arg: string) { + // arg 不会为null、undefined, 所以 ?. 操作符是不必要的 + return arg?.length; +} +``` + +### [推荐]避免不必要的限定符 + +解释: + +在TypeScript中,enum和namespace的成员通常通过限定属性的方式进行访问,例如`Enum.member`。但是当在enum或namespace的内部访问其成员时,可以不加限定符。 + +你可以使用以下 ESLint 规则进行自动化检测: + +```json +{ + "@typescript-eslint/no-unnecessary-qualifier": "warn" +} +``` + +示例: + +```ts +// good +enum A { + B, + C = B, +} +namespace A { + export type B = number; + const x: B = 3; +} + +// bad +enum A { + B, + // 在枚举A内部访问B,可以直接访问B,而不是通过A.B的方式 + C = A.B, +} +namespace A { + export type B = number; + // 在命名空间A内部访问B,可以直接访问B,而不是通过A.B的方式 + const x: A.B = 3; +} +``` + +### [推荐]避免不必要的类型参数 + +解释: + +在TypeScript中定义函数、接口、类时可以为类型参数指定一个默认值,例如 `function f(...) {...}`,在使用它的时候如果将要显式指定的类型参数的值,如`f(...)`等于该类型参数的默认值,则无需显式指定。 + +你可以使用以下 ESLint 规则进行自动化检测: + +```json +{ + "@typescript-eslint/no-unnecessary-type-arguments": "warn" +} +``` + +示例: + +```ts +// good +class C {} +new C(); +// 类型参数默认值是number,此处可以显示指定为string +new C(); + +class D extends C {} +// 类型参数默认值是number,此处可以显示指定为string +class D extends C {} + +interface I {} +// 类型参数默认值是number,此处可以显示指定为string +class Impl implements I {} + +// bad +class C {} +// 类型参数默认值是number,此处无需指定 +new C(); + +class D extends C {} +// 类型参数默认值是number,此处无需指定 +interface I {} +class Impl implements I {} +``` + +### [强制]禁止不必要的类型断言 + +解释: + +在TypeScript中可以使用类型断言来告诉编译器当前表达式的类型,但是如果断言的结果与编译器预期的类型相同,则应该避免将断言留在代码中,这会影响可读性。 + +你可以使用以下 ESLint 规则进行自动化检测: + +```json +{ + "@typescript-eslint/no-unnecessary-type-assertion": "error" +} +``` + +示例: + +```ts +// good +const foo = 3; + +const foo = 3 as number; + +// 此处存疑,@see https://typescript-eslint.io/rules/no-unnecessary-type-assertion +// 是否应该作为bad case的示例 +const foo = 'foo' as const; + +function foo(x: number | undefined): number { + // 此处非null和非undefined的类型断言是有必要的 + return x!; +} + +// bad +const foo = 3; +const bar = foo!; + +const foo = <3>3; + +type Foo = 3; +const foo = 3; + +type Foo = 3; +const foo = 3 as Foo; + +function foo(x: number): number { + // 此处非null和非undefined的类型断言是不必要的 + return x!; +} +``` + +### [强制]禁止不必要的类型约束 + +解释: + +在TypeScript中泛型参数可以被关键字`extends`约束,当没有提供`extends`关键字时,默认为`extends any`,因此手动的`extends any`是不必要的。 + +你可以使用以下 ESLint 规则进行自动化检测: + +```json +{ + "@typescript-eslint/no-unnecessary-type-constraint": "error" +} +``` + +示例: + +```ts +// good +interface Foo {} + +type Bar = {}; + +class Baz { + qux { } +} + +const Quux = () => {}; + +function Quuz() {} + +// bad +interface FooAny {} + +type BarAny = {}; + +class BazAny { + quxAny() {} +} + +const QuuxAny = () => {}; + +function QuuzAny() {} +``` + +### [强制] 禁止在函数调用时使用any类型的参数 + +解释: + +在TypeScript中使用any通常是一个危险的信号,any会导致跳过许多类型检查规则,因此在函数调用时使用any类型的参数会产生潜在的安全漏洞和错误。涉及这一规则的场景有: + +- 函数调用时禁止使用any类型的参数 +- 函数调用时禁止使用包含any类型元素的数组或元祖 +- 禁止将any类型传递给明确指定类型的泛型参数 + +允许将any类型的参数传递给unknow类型,例如: + +```js +declare function foo(arg1: unknown, arg2: Set, arg3: unknown[]): void; +foo(1 as any, new Set(), [] as any[]); +``` + +你可以使用以下 ESLint 规则进行自动化检测: + +```json +{ + "@typescript-eslint/no-unsafe-argument": "error" +} +``` + +示例: + +```ts +// good +declare function foo(arg1: string, arg2: number, arg3: string): void; + +foo('a', 1, 'b'); + +const tuple1 = ['a', 1, 'b'] as const; +foo(...tuple1); + +declare function bar(arg1: string, arg2: number, ...rest: string[]): void; +const array: string[] = ['a']; +bar('a', 1, ...array); + +declare function baz(arg1: Set, arg2: Map): void; +foo(new Set(), new Map()); +// bad +declare function foo(arg1: string, arg2: number, arg3: string): void; + +const anyTyped = 1 as any; + +foo(...anyTyped); +foo(anyTyped, 1, 'a'); + +const anyArray: any[] = []; +foo(...anyArray); + +const tuple1 = ['a', anyTyped, 'b'] as const; +foo(...tuple1); + +const tuple2 = [1] as const; +foo('a', ...tuple, anyTyped); + +declare function bar(arg1: string, arg2: number, ...rest: string[]): void; +const x = [1, 2] as [number, ...number[]]; +foo('a', ...x, anyTyped); + +declare function baz(arg1: Set, arg2: Map): void; +foo(new Set(), new Map()); +``` + +### [强制]禁止使用不安全的赋值 + +解释: + +在 `TypeScript`中 `any`是一个危险的事情,使用它会跳过很多类型检测,因此最好作为最后的招数,或者仅 demo 代码中使用。尽管你初衷是好的,但 `any` 类型很容易造成代码漏洞,另外将 `any` 赋值给变量之后会变得很难理解。 + +此规则不允许将 `any` 分配给变量,或者将 `any[]` 分配给数组。此规则还会针对泛型类型的参数进行检查,例如,如果将 `Set` 分配给声明为 `Set`的变量的时候,就会报错。 + +你可以使用以下 ESLint 规则进行自动化检测: + +```json +{ + "@typescript-eslint/no-unsafe-assignment": "error" +} +``` + +示例: + +```ts +// good +const x = 1, y = 1; +const [x] = [1]; +[x] = [1] as [number]; + +function foo(a = 1) {} +class Foo { + constructor(private a = 1) {} +} +class Foo { + private a = 1; +} + +const x: Set = new Set(); +const x: Map = new Map(); +const x: Set = new Set(); +const x: Set>> = new Set>>(); + +// bad +const x = 1 as any, y = 1 as any; +const [x] = 1 as any; +const [x] = [] as any[]; +const [x] = [1 as any]; +[x] = [1] as [any]; + +function foo(a = 1 as any) {} +class Foo { + constructor(private a = 1 as any) {} +} +class Foo { + private a = 1 as any; +} + +const x: Set = new Set(); +const x: Map = new Map(); +const x: Set = new Set(); +const x: Set>> = new Set>>(); +``` + +以下这种场景,将 `any`赋值给 `unknown`是可以的。 + +```ts +const x: unknown = y as any; +const x: unknown[] = y as any[]; +const x: Set = y as Set; +``` + +### [强制]禁止使用不安全的调用 + +解释: + +在 `TypeScript`中 `any`是一个危险的事情,使用它会跳过很多类型检测,因此最好作为最后的招数,或者仅 demo 代码中使用。尽管你初衷是好的,但 `any` 类型很容易造成代码漏洞,另外将 `any` 赋值给变量之后会变得很难理解。 + +此规则不允许调用类型为 any 的方法。 + +你可以使用以下 ESLint 规则进行自动化检测: + +```json +{ + "@typescript-eslint/no-unsafe-call": "error" +} +``` + +示例: + +```ts +// good +declare const typedVar: () => void; +declare const typedNested: { prop: { a: () => void } }; + +typedVar(); +typedNested.prop.a(); + +(() => {})(); + +new Map(); + +String.raw`foo`; + +// bad +declare const anyVar: any; +declare const nestedAny: { prop: any }; + +anyVar(); +anyVar.a.b(); + +nestedAny.prop(); +nestedAny.prop['a'](); + +new anyVar(); +new nestedAny.prop(); + +anyVar`foo`; +nestedAny.prop`foo`; +``` + +### + +### [强制]禁止使用不安全的声明合并 + +解释: + +在 `TypeScript`中,支持对相同的命名进行声明合并。但是类和接口之间的声明合并是不安全的。 + +因为 `TypeScript` 编译器不会检查属性是否已经初始化,就会导致这些代码运行时发生错误。 + +```ts +interface Foo { + nums: number[]; +} + +class Foo {} + +const foo = new Foo(); + +foo.nums.push(1); // Runtime Error: Cannot read properties of undefined. +``` + +你可以使用以下 ESLint 规则进行自动化检测: + +```json +{ + "@typescript-eslint/no-unsafe-declaration-merging": "error" +} +``` + +示例: + +```ts +// good +interface Foo {} +class Bar implements Foo {} + +namespace Baz {} +namespace Baz {} +enum Baz {} + +namespace Qux {} +function Qux() {} + +// bad +interface Foo {} + +class Foo {} +``` + +### + +### [强制]禁止访问不安全的成员 + +解释: + +在 `TypeScript`中 `any`是一个危险的事情,使用它会跳过很多类型检测,因此最好作为最后的招数,或者仅 demo 代码中使用。尽管你初衷是好的,但 `any` 类型很容易造成代码漏洞,另外将 `any` 赋值给变量之后会变得很难理解。 + +此规则禁止访问 `any` 类型的成员。 + +你可以使用以下 ESLint 规则进行自动化检测: + +```json +{ + "@typescript-eslint/no-unsafe-member-access": "error" +} +``` + +示例: + +```ts +// good +declare const properlyTyped: { prop: { a: string } }; + +properlyTyped.prop.a; +properlyTyped.prop['a']; + +const key = 'a'; +properlyTyped.prop[key]; + +const arr = [1, 2, 3]; +arr[1]; +const idx = 1; +arr[idx]; +arr[idx++]; + + +// bad +declare const anyVar: any; +declare const nestedAny: { prop: any }; + +anyVar.a; +anyVar.a.b; +anyVar['a']; +anyVar['a']['b']; + +nestedAny.prop.a; +nestedAny.prop['a']; + +const key = 'a'; +nestedAny.prop[key]; + +// Using an any to access a member is unsafe +const arr = [1, 2, 3]; +arr[anyVar]; +nestedAny[anyVar]; +``` + +### + +### [强制]禁止返回不安全的内容 + +解释: + +在 `TypeScript`中 `any`是一个危险的事情,使用它会跳过很多类型检测,因此最好作为最后的招数,或者仅 demo 代码中使用。尽管你初衷是好的,但 `any` 类型很容易造成代码漏洞,另外将 `any` 赋值给变量之后会变得很难理解。 + +此规则禁止函数返回 `any`或者 `any[]`,另外也会针对泛型类型的参数进行检查,例如,如果函数定义返回是 `Set`的时候,实际返回了 `Set` 也会报错。 + +你可以使用以下 ESLint 规则进行自动化检测: + +```json +{ + "@typescript-eslint/no-unsafe-return": "error" +} +``` + +示例: + +```ts +// good +function foo1() { + return 1; +} +function foo2() { + return Object.create(null) as Record; +} + +const foo3 = () => []; +const foo4 = () => ['a']; + +function assignability1(): Set { + return new Set(['foo']); +} +type TAssign = () => Set; +const assignability2: TAssign = () => new Set(['foo']); + + +// bad +function foo1() { + return 1 as any; +} +function foo2() { + return Object.create(null); +} +const foo3 = () => { + return 1 as any; +}; +const foo4 = () => Object.create(null); + +function foo5() { + return [] as any[]; +} +function foo6() { + return [] as Array; +} +function foo7() { + return [] as readonly any[]; +} +function foo8() { + return [] as Readonly; +} +const foo9 = () => { + return [] as any[]; +}; +const foo10 = () => [] as any[]; + +const foo11 = (): string[] => [1, 2, 3] as any[]; + +// generic position examples +function assignability1(): Set { + return new Set([1]); +} +type TAssign = () => Set; +const assignability2: TAssign = () => new Set([true]); +``` + +不过,定义 `unknown`实际返回 `any`是可以的,例如以下这种场景 + +```ts +function foo1(): unknown { + return JSON.parse(singleObjString); // Return type for JSON.parse is any. +} + +function foo2(): unknown[] { + return [] as any[]; +} +``` + +### [强制]禁止导出无用的空字符集 + +解释: + +空的 `export {}` 语句有时在 `TypeScript` 代码中很有用,可以将本来是脚本文件的文件转换为模块文件。参考 [TypeScript Handbook Modules page](https://www.typescriptlang.org/docs/handbook/modules.html)。但是,如果文件中有任何其他顶级导入或导出语句,则 `export {}` 语句不会执行任何操作。此条规则禁止再 `ES Module`中导出无用的 `export {}`语句。 + +你可以使用以下 ESLint 规则进行自动化检测: + +```json +{ + "@typescript-eslint/no-useless-empty-export": "error" +} +``` + +示例: + +```ts +// good +export const value = 'Hello, world!'; + +import 'some-other-module'; + +// bad +export const value = 'Hello, world!'; +export {}; + +import 'some-other-module'; +export {}; +``` + +### [强制] 禁止在非模块导入语句中使用 `require` + +解释: + +禁止使用类似 `var foo = require("foo")`的语句。应当使用 ES6 样式的模块导入或 `import foo = require("foo")`的形式替代。 + +你可以使用以下 ESLint 规则进行自动化检测: + +```json +{ + "@typescript-eslint/no-var-requires": "error" +} +``` + +示例: + +```ts +// good +import foo = require('foo'); +require('foo'); +import foo from 'foo'; + +// bad +var foo = require('foo'); +const foo = require('foo'); +let foo = require('foo'); +``` + +### [推荐] 有明确类型定义时应使用非空断言 + +解释: + +在 TypeScript 中断言一个不是 `null` 或 `undefined` 的值通常有两种方法: + +- `!` : 非空断言 +- `as` : 使用等效类型的传统类型断言 + +一般来说使用 `!` 非空断言更好。因为代码量更少,并且当类型变化时不会无法同步。当 `as` 关键字与 `!` 起到相同的作用时,将触发本条规则,并建议用 `!` 修复代码。 + +你可以使用以下 ESLint 规则进行自动化检测: + +```json +{ + "@typescript-eslint/non-nullable-type-assertion-style": "warn" +} +``` + +示例: + +```ts +// good +const maybe = Math.random() > 0.5 ? '' : undefined; + +const definitely = maybe!; +const alsoDefinitely = maybe!; + +// bad +const maybe = Math.random() > 0.5 ? '' : undefined; + +const definitely = maybe as string; +const alsoDefinitely = maybe; +``` + +### [强制] 字面类型强制使用 `as const` + +解释: + +有两种常用的方法告诉 TypeScript 一个字面量应当被解释为字面类型本身(例如 `2`)而不是一般的通用类型(例如 `number`): + +- `as const` : 让 TypeScript 自动推断字面类型 +- `as` 加字面类型: 为 TypeScript 显式指定字面类型 + +一般来说更加推荐 `as const`,因为无需再次输入一遍字面值。当 `as` 加字面类型可以被 `as const` 替代时,将触发本规则。 + +你可以使用以下 ESLint 规则进行自动化检测: + +```json +{ + "@typescript-eslint/prefer-as-const": "error" +} +``` + +示例: + +```ts +// good +let foo = 'bar'; +let foo = 'bar' as const; +let foo: 'bar' = 'bar' as const; +let bar = 'bar' as string; +let foo = 'bar'; +let foo = {bar: 'baz'}; + +// bad +let bar: 2 = 2; +let foo = <'bar'>'bar'; +let foo = {bar: 'baz' as 'baz'}; +``` + +### [推荐] 每个枚举成员必须被显式初始化 + +解释: + +在 TypeScript 中使用 `enum` 来组织语义化相关的常量是一个很实用的方法。`enum` 中没有指定具体值的枚举成员默认会被赋予一个自增的数字。 + +当一个项目中 `enum` 枚举成员的值很重要时,如果 `enum` 之后被修改,允许包含隐含值的枚举可能会造成缺陷。 + +本规则建议每个 `enum` 成员都显式指定初始化的值。 + +你可以使用以下 ESLint 规则进行自动化检测: + +```json +{ + "@typescript-eslint/prefer-enum-initializers": "warn" +} +``` + +示例: + +```ts +// good +enum Status { + Open = 'Open', + Close = 'Close' +} + +enum Direction { + Up = 1, + Down = 2 +} + +enum Color { + Red = 'Red', + Green = 'Green', + Blue = 'Blue' +} + +// bad +enum Status { + Open = 1, + Close +} + +enum Direction { + Up, + Down +} + +enum Color { + Red, + Green = 'Green' + Blue = 'Blue' +} +``` + +### [推荐] 尽可能使用 `for-of` 循环而不是标准 `for` 循环 + +解释: + +许多开发者默认用 `for (let i = 0; i < ...` 循环来遍历数组。然而,这些数组中很大一部分的循环遍历变量(例如 `i`)只是用来访问数组中每个元素。这种情况下,`for-of` 循环读写起来更加容易。 + +本规则建议当循环索引只是用来访问被遍历数组的时候,应当使用 for-of 循环。 + +你可以使用以下 ESLint 规则进行自动化检测: + +```json +{ + "@typescript-eslint/prefer-for-of": "warn" +} +``` + +示例: + +```ts +// good +declare const array: string[]; + +for (const x of array) { + console.log(x); +} + +for (let i = 0; i < array.length; i++) { + // 使用到了 i,因此可以不用 for-of + console.log(i, array[i]); +} + +// bad +declare const array: string[]; + +for (let i = 0; i < array.length; i++) { + console.log(array[i]); +} +``` + +### [推荐] 使用函数类型而不是含有调用签名的接口 + +解释: + +TypeScript 允许两种常见的方法来定义一个函数的类型: + +- 函数类型: `() => string` +- 含有签名的对象: `{(): string}` + +通常应当尽可能使用函数类型方式,因为这样更加简单明了。 + +本规则建议使用函数类型而不是含有调用签名的接口或对象类型。 + +你可以使用以下 ESLint 规则进行自动化检测: + +```json +{ + "@typescript-eslint/prefer-function-type": "error" +} +``` + +示例: + +```ts +// good +type Example = () => string; + +function foo(example: () => number): number { + return bar(); +} + +// 返回函数本身,而不是 `this` 参数。 +type ReturnsSelf = (arg: string) => ReturnsSelf; + +function foo(bar: {(): string; baz: number}): string { + return bar(); +} + +interface Foo { + bar: string; +} +interface Bar extends Foo { + (): void; +} + +// 允许使用多个调用签名(重载): +interface Overloaded { + (data: string): number; + (id: number): string; +} +// 等同于重载的接口 +type Intersection = ((data: string) => number) & ((id: number) => string); + +// bad +interface Example { + (): string; +} + +function foo(example: {(): number }): number { + return example(); +} + +interface ReturnsSelf { + // 返回函数本身,而不是 `this` 参数。 + (arg: string): this; +} +``` + +### [推荐]推荐使用`includes` + +解释: + +在ES2015规范发布之前,检测数组和字符串中是否存在某个值的标准方法是判断`Array#indexOf` 和 `String#indexOf`是否等于`-1`。 +在ES2015规范之后,增加了`String#includes`(ES2015)和`Array#includes`(ES2016)来检测数组和字符串中是否存在某个值。 + +为了代码的可读性,本条规则推荐使用`.includes`方法替代之前的`indexOf`方法。此外,同样推荐使用`.includes`方法替代简单的正则表达式校验方法。 + +> 本条规则同样适用于同时拥有`indexOf`和`includes`方法且两个方法具有相同入参类型的自定义对象 + +检查判断String中是否存在在ES2015之前 + +你可以使用以下 ESLint 规则进行自动化检测: + +```json +{ + "@typescript-eslint/prefer-includes": "warn" +} +``` + +示例: + +```ts +// good +const str: string; +const array: any[]; +const readonlyArray: ReadonlyArray; +const typedArray: UInt8Array; +const maybe: string; +const userDefined: { + indexOf(x: any): number; + includes(x: any): boolean; +}; + +str.includes(value); +array.includes(value); +readonlyArray.includes(value); +typedArray.includes(value); +maybe?.includes(''); +userDefined.includes(value); + +str.includes('example'); + +// 如果自定义的 indexOf 和 includes 方法的入参类型不一致,则本条规则不适用 +declare const mismatchExample: { + indexOf(x: unknown, fromIndex?: number): number; + includes(x: unknown): boolean; +}; +mismatchExample.indexOf(value) >= 0; + +// bad +const str: string; +const array: any[]; +const readonlyArray: ReadonlyArray; +const typedArray: UInt8Array; +const maybe: string; +const userDefined: { + indexOf(x: any): number; + includes(x: any): boolean; +}; + +str.indexOf(value) !== -1; +array.indexOf(value) !== -1; +readonlyArray.indexOf(value) === -1; +typedArray.indexOf(value) > -1; +maybe?.indexOf('') !== -1; +userDefined.indexOf(value) >= 0; + +/example/.test(str); +``` + +### [强制]使用常量枚举类型 + +解释: + +TypeScript允许枚举类型的值是有效的js表达式。但由于枚举内部是单独的作用域,且每个枚举类型是当前作用域中的一个变量,所以会产生一些违反直觉的结果。 +例如: + +```ts +const imOutside = 2; +const b = 2; +enum Foo { + outer = imOutside, + a = 1, + b = a, + c = b, + // 此时c的值是什么 + // c == Foo.b == Foo.c == 1? + // c == b == 2? +} +``` + +> 答案: `c` 等于 `1` + +因此,为了防止出现这些问题,此规则要求使用常量作为枚举类型的值,但允许常量进行位运算。 + +你可以使用以下 ESLint 规则进行自动化检测: + +```json +{ + "@typescript-eslint/prefer-literal-enum-member": ["error", {"allowBitwiseExpressions": true}] +} +``` + +示例: + +```ts +// good +enum Valid { + A, + B = 'TestStr', // 字符串类型 + C = 4, // 数字类型 + D = null, + E = /some_regex/, // 正则表达式 + F = 0 << 2, // 常量位运算 +} + + +// bad +const str = 'Test'; +const num = 0; +enum Invalid { + A = str, // 变量 + B = {}, // 对象 + C = `A template literal string`, // 模板字面量 + D = new Set(1, 2, 3), // 构造函数 + E = 2 + 2, // 表达式 + F = num << 1; // 非常量位运算 +} +``` + +### [推荐] 使用空值合并运算符 + +解释: + +空值合并运算符(??)是一个逻辑运算符,当左侧的操作数为 null 或者 undefined 时,返回其右侧操作数,否则返回左侧操作数。 + +逻辑或运算符(||)会在左侧操作数为假值时返回右侧操作数。 + +所以在处理假值相关的操作时,本条规则推荐使用空值合并运算符代替逻辑或运算符。 + +你可以使用以下 ESLint 规则进行自动化检测: + +```json +{ + "@typescript-eslint/prefer-nullish-coalescing": "warn" +} +``` + +示例: + +```ts +// good +const foo: any = 'bar'; +foo ?? 'a string'; + +const foo: string | undefined = 'bar'; +foo ?? 'a string'; + +const foo: string | null = 'bar'; +foo ?? 'a string'; + +declare const a: string | null; +declare const b: string | null; +declare const c: string | null; + +if (a ?? b) { +} +while (a ?? b) {} +do {} while (a ?? b); +for (let i = 0; a ?? b; i += 1) {} +a ?? b ? true : false; + +// 由于 ?? 运算符的没有算数优先级,所以需要使用括号隔离??运算符 +a ?? (b && c); + + +// bad +``` + +### [推荐]优先使用可选链运算符 @常宇清 + +解释: + +可选链表达式(`?.`)在访问值为`null`或者`undfined`的对象时,会返回`undfined`。并且相对于逻辑与运算符(`&&`)来说,可选运算符写法更加简洁。 +本条规则推荐使用`?.`替代`&&` + +你可以使用以下 ESLint 规则进行自动化检测: + +```json +{ + "@typescript-eslint/prefer-optional-chain": "warn" +} +``` + +示例: + +```ts +// good +foo?.a?.b?.c; +foo?.['a']?.b?.c; +foo?.a?.b?.method?.(); + +foo?.a?.b?.c?.d?.e; + +!foo?.bar; +!foo?.[bar]; +!foo?.bar?.baz?.(); + + +// bad +foo && foo.a && foo.a.b && foo.a.b.c; +foo && foo['a'] && foo['a'].b && foo['a'].b.c; +foo && foo.a && foo.a.b && foo.a.b.method && foo.a.b.method(); + +// With empty objects +(((foo || {}).a || {}).b || {}).c; +(((foo || {})['a'] || {}).b || {}).c; + +// With negated `or`s +!foo || !foo.bar; +!foo || !foo[bar]; +!foo || !foo.bar || !foo.bar.baz || !foo.bar.baz(); + +// this rule also supports converting chained strict nullish checks: +foo && + foo.a != null && + foo.a.b !== null && + foo.a.b.c != undefined && + foo.a.b.c.d !== undefined && + foo.a.b.c.d.e; +``` + +### [推荐]优先使用readonly属性 @常宇清 + +解释: + +如果一个成员变量被标记为`private`,则这个变量不应该在类定义之外访问,同时,如果这个私有成员变量只在构造函数中被修改,则他同时应当被标记为`readonly`。 +本条规则推荐使用`readonly`标记只在构造函数中修改的私有成员变量。 + +你可以使用以下 ESLint 规则进行自动化检测: + +```json +{ + "@typescript-eslint/prefer-readonly": "warn" +} +``` + +示例: + +```ts +// good +class Container { + // 公共成员变量有可能被外部修改 + public publicMember: boolean; + + // 受保护的成员变量有可能被子类修改 + protected protectedMember: number; + + // 私有成员变量有可能被当前类的非构造函数修改 + private modifiedLater = 'unchanged'; + + // 如果当前私有成员变量没有被当前类的非构造函数修改,则需要加上`readonly` + private readonly neverModifiedMember = true; + + public mutate() { + this.modifiedLater = 'mutated'; + } +} + +// bad +class Container { + // 这个成员变量应该标记为`readonly` + private neverModifiedMember = true; + private onlyModifiedInConstructor: number; + + public constructor( + onlyModifiedInConstructor: number, + // 私有形参同样应该标记为`readonly` + private neverModifiedParameter: string, + ) { + this.onlyModifiedInConstructor = onlyModifiedInConstructor; + } +} +``` + +### [推荐]当返回当前类型时 ,优先使用`this` + +解释: + +方法链是 OOP 语言中一种常见的设计模式,Typescript为这种模式提供了一个动态的`this`类型。当类方法显示声明返回值类型为当前类时,会导致当前类的子类调用该方法时返回的值是父类的类型,而不是子类的类型。例如: + +```ts +class Animal { + eat(): Animal { + // ~~~~~~ + // 为了避免下面这个错误, + // 你可以移除这个声明 + // 或者使用`this`替代它 + console.log("I'm moving!"); + return this; + } +} + +class Cat extends Animal { + meow(): Cat { + console.log('Meow~'); + return this; + } +} + +const cat = new Cat(); +cat.eat().meow(); +// ~~~~ +// Error: Property 'meow' does not exist on type 'Animal'. +// 因为`eat`方法返回的是`Animal`类型,但`Animal`类型中不存在`meow`方法 +``` + +本条规则推荐使用`this`类型替代显示声明当前类的类型 + +你可以使用以下 ESLint 规则进行自动化检测: + +```json +{ + "@typescript-eslint/prefer-return-this-type": "warn" +} +``` + +示例: + +```ts +// good +class Foo { + f1(): this { + return this; + } + f2() { + return this; + } + f3 = (): this => { + return this; + }; + f4 = () => { + return this; + }; +} + +class Base {} +class Derived extends Base { + f(): Base { + return this; + } +} +// bad +class Foo { + f1(): Foo { + return this; + } + f2 = (): Foo => { + return this; + }; + f3(): Foo | undefined { + return Math.random() > 0.5 ? this : undefined; + } +} +``` + +### [推荐]优先使用String#startsWith和 String#endsWith + +解释: + +JS中有多种方式判断字符串string是否由特定字符串开头或结尾,例如:foo.indexOf('bar') === 0。ES5中推荐使用String#startsWith和 String#endsWith来判断。统一使用这些方法来判断可提高代码可读性。 + +当代码中的方法完全可以使用String#startsWith或 String#endsWith 替代时则会命中此规则。此规则无其他配置项。 + +```json +{ + "@typescript-eslint/prefer-string-starts-ends-with": "warn" +} +``` + +示例: + +```ts +// good +declare const foo: string; + +// starts with +foo.startsWith('bar'); + +// ends with +foo.endsWith('bar'); + +// bad +declare const foo: string; + +// starts with +foo.indexOf('bar') === 0; +foo.slice(0, 3) === 'bar'; +foo.substring(0, 3) === 'bar'; +foo.match(/^bar/) != null; +/^bar/.test(foo); + +// ends with +foo[foo.length - 1] === 'b'; +foo.charAt(foo.length - 1) === 'b'; +foo.lastIndexOf('bar') === foo.length - 3; +``` + +### [强制]强制使用ts-expect-error替代ts-ignore + +解释: + +TypeScript可以通过在代码行开头添加@ts-ignore和@ts-expect-error标记豁免单行代码的TS报错信息。这两个标记的表现基本一致,主要区别在,@ts-expect-error如果在正确的代码行前标记会抛出type error,@ts-ignore则不会。 + +因此即便问题代码已修复,原有的@ts-ignore 易被遗漏,仍留在代码中。若出现新的问题又会被遗留的@ts-ignore 忽略,不能及时修复。推荐优先使用@ts-expect-error。 + +```json +{ + "@typescript-eslint/prefer-ts-expect-error": "error" +} +``` + +示例: + +```ts +// good + +// @ts-expect-error +const str: string = 1; + +/** + * Explaining comment + * + * @ts-expect-error */ +const multiLine: number = 'value'; + +/** @ts-expect-error */ +const block: string = 1; + +const isOptionEnabled = (key: string): boolean => { + // @ts-expect-error: if key isn't in globalOptions it'll be undefined which is false + return !!globalOptions[key]; +}; + +// bad + +// @ts-ignore +const str: string = 1; + +/** + * Explaining comment + * + * @ts-ignore */ +const multiLine: number = 'value'; + +/** @ts-ignore */ +const block: string = 1; + +const isOptionEnabled = (key: string): boolean => { + // @ts-ignore: if key isn't in globalOptions it'll be undefined which is false + return !!globalOptions[key]; +}; +``` + +### [强制] 限制加号运算符的操作值类型 + +解释: + +TypeScript允许『 + 』运算符对任意类型的两个数值进行相加。但是,如果让原始数据类型不同的两个数值相加容易导致异常。 + +当『 + 』运算符操作的两个值为不同类型,或者操作值的类型不为bigint/number/string时,该规则会进行限制或提示。 + +```json +{ + "@typescript-eslint/restrict-plus-operands": [ + "error", + { + // 是否检查复合运算符,如 `+=`. 默认false + "checkCompoundAssignments": false, + // 是否允许使用 `any` 类型. 默认false + "allowAny": false, + } + ] +} +``` + +示例: + +```ts +// good + +// 默认配置 +var foo = parseInt('5.5', 10) + 10; +var foo = 1n + 1n; + +/*eslint @typescript-eslint/restrict-plus-operands: ["error", { "checkCompoundAssignments": true }]*/ +let foo: number = 0; +foo += 1; + +// { allowAny: true } +var fn = (a: any, b: string) => a + b; + +// bad + +// 默认配置 +var foo = '5.5' + 5; +var foo = 1n + 1; + +/*eslint @typescript-eslint/restrict-plus-operands: ["error", { "checkCompoundAssignments": true }]*/ +let bar: string = ''; +bar += 0; + +// { allowAny: true } +var fn = (a: any, b: {}) => a + b; +``` + +### [强制]强制模板字符串中使用string类型 + +解释: + +在JavaScript有些场景下会默隐式调用对象的toString()方法将其转换为字符串,比如当使用+操作符拼接字符,或者在${}模板语法中。Object对象的toString()方法默认返回的`"[object Object]"`字符串,往往不符合预期。这条规则当模板字符串中的数值既不是基础数据类型也没有toString()方法时则生效。 + +```json +{ + "@typescript-eslint/restrict-template-expressions": [ + "error", + { + // 模板字符串中是否允许number类型,默认true + "allowNumber": true, + // 模板字符串中是否允许boolean类型,默认false + "allowBoolean": true, + // 模板字符串中是否允许any类型,默认false + "allowAny": false, + // 模板字符串中是否允许null,默认false + "allowNullish": false, + // 模板字符串中是否允许正则表达式,默认false + "allowRegExp": true + } + ] +} +``` + +示例: + +```ts +// good + +const stringWithKindProp: string & { _kind?: 'MyString' } = 'foo'; +const msg3 = `stringWithKindProp = ${stringWithKindProp}`; + +// bad + +const arg1 = [1, 2]; +const msg1 = `arg1 = ${arg1}`; +``` + +### [推荐]switch-case所有场景需进行联合类型定义 + +解释: + +TypeScript中的联合类型经常用于描述,switch-case语句中可能包含的case场景的类型的集合。但若联合类型变化了,往往容易忘记修改对应的case。 + +此规则当switch语句中缺少default语句,或者不指定default且未将case对应类型的所有场景列出时会提示报错。 + +```json +{ + "@typescript-eslint/switch-exhaustiveness-check": "warn" +} +``` + +示例: + +```ts +type Day = + | 'Monday' + | 'Tuesday'; + +const day = 'Monday' as Day; +let result = 0; + +// good + +switch (day) { + case 'Monday': + result = 1; + break; + default: + result = 42; +} + +switch (day) { + case 'Monday': + result = 1; + break; + case 'Tuesday': + result = 2; + break; +} + + +// bad + +switch (day) { + case 'Monday': + result = 1; + break; +} +``` + +### [强制]类型声明前后空格需要保持一致 + +解释: + +类型声明前后空格一致可提高代码可读性。TypeScript中最常见的类型声明空格样式规范是在冒号后添加一个空格,有其他特殊需求也可通过配置项定制。 + +```json +{ + "@typescript-eslint/type-annotation-spacing": [ + "error", + { + "before": false, + "after": true, + "overrides": { + "arrow": {"before": true, "after": true} + } + } + ] +} +``` + +示例: + +```js +// good +let foo: string = "bar"; +let foo: string = () => {}; + +// bad +let foo:string = "bar"; +function foo() :string {} +let foo: string = ()=>{}; +```