概要
你可能在代码中见过各种稀奇古怪的注释,丰富类型堪比各种稀奇古怪的代码。其实,无论是代码或者注释,都是有规范的,无论是官方的还是团队内定义的。我今天要介绍的JSDoc就是JavaScript的官方代码注释规范。
无论是那个前端IDE/编辑器,都是基于此规范来解析注释的,所以,如果你的注释不规范,不但你的小伙伴看不到,可能编辑器也看不懂。
为了避免这个尴尬,我们一起来看下正确的注释应该如何去写。
常见场景
JSDoc最简单的使用方式就是让编辑器自动生成。触发方式很简单:输入/** ,按下 Tab即可自动生成JSDoc风格的注释代码。
原始值
原始值的注释很简单,我们可以从中学习到JSDoc的注释风格,以及@type标签。
类型名称在注释中均为小写
/**
* 最小值。数字
* @type {number}
*/
const MIN_VALUE = 1
// MIN_VALUE
/**
* 临时名称。字符串
* @type {string}
*/
let tempName = 'joe'
// tempName
/**
* 是否显示。布尔
* @type {boolean}
*/
let isShow = false
// isShow
/**
* 空对象
* @type {null}
*/
let hello = null
// hello
/**
* 未定义
* @type {undefined}
*/
const last = undefined
// last
数组
对于子项同类型的注释也很简单,与TypeScript类似,格式是 type[]。
/**
* 字符串列表
* @type {string[]}
*/
const wordList = ['ai', 'box', 'cell', 'delete']
// wordList
对象
对于内置对象,直接使用对象的类型,IDE会自动给予代码提示。
/**
* 时间对象
* @type {Date}
*/
const ins = new Date()
// ins.getDay()
/**
* promise对象
* @type {Promise<string>}
*/
const promise = new Promise((resolve => resolve('hello')))
// promise.then()
对于自定义对象类型,可以在类型中直接写出定义,注释需要写在属性上。
vscode对于以下写法的注释解析有问题【issue】,webstorm正常。
/**
* 自定义对象
* @type {{x: number, y: ((function(number): string)|*), z: {a: number}}}
*/
const position = {
/**
* 横轴坐标
*/
x: 1,
/**
* 竖轴坐标
* @param {number} data 一个入参
* @return {string} 返回值
*/
y: function (data) {},
/**
* 立体坐标
* @type {{a: number}}
*/
z: {
/**
* 一个属性
*/
a: 3
}
}
// position.y
上面的写法对于简单的对象还行。但是如果对象非常复杂,就会有问题,主要有两个问题:① 类型注释冗杂在一行内,② 描述注释跟类型注释是分开的。
下面我们请出JSDoc中拓展性最强的标签组合@typedef和@property来解决复杂东西中会出现的这两个问题。
/**
* @typedef ComplexObj
* @property {object} a 对象a
* @property {object} a.b 对象a.b
* @property {number} a.b.c 数字a.b.c
* @property {object} y 有一个对象y
* @property {string} y.x 字符串
* @property {string[]} z 字符串数组
*/
/**
* 复杂的对象
* @type {ComplexObj}
*/
const complexObj = {
a: {
b: {
c: 1
}
},
y: {
x: 'hello'
},
z: ['r', 'y']
}
// complexObj.y.x
数组对象也差不多。我们顺便学习一下类型操作符,即使用 |链接两个类型。以及 []表示该属性是可选的。
/**
* @typedef Animal 动物
* @property {string} name 名称
* @property {string} [live] 名称
* @property {number | string} foot 脚
*/
/**
* 动物列表
* @type {Animal[]}
*/
const animalList = [
{
name: 'cat',
live: 'land',
foot: 4
},
{
name: 'fish',
live: 'water',
foot: 0
},
{
name: 'dragon',
foot: 'some'
}
]
// animalList[0].live
混合数组与对象。
webstorm中的代码提示对数组解析有问题【issue】,vscode正常。
/**
* @typedef School 学习
* @property {string} name 名称
* @property {object[]} class 班级
* @property {number} class[].build 教室
* @property {string} class[].students 学生人数
* @property {object[]} class[].examples 模范学生
* @property {string} class[].examples[].name 学生名字
* @property {number} class[].examples[].age 学生年龄
*/
/**
* 数组对象混合
* @type {School}
*/
const school = {
name: 'North Big',
class: [
{
build: 4,
students: '1w+',
examples: [
{
name: 'No.1',
age: 43
}
]
}
]
}
// school.class[0].examples[0].age
函数
函数的注释一般包括入参(param)和出参(return)。入参可以有多个。
/**
* 函数,待办事项
* @param {string} thing 加如一个待办
* @param {number} index 所以
* @return {string[]}
*/
function toDo (thing, index) {
const list = ['pay apple']
list[index] = thing
return list
}
let todo = toDo('buy dog', 3)
类
一般无需对类进行特别的注释,IDE都会自动生成对应的文档。只需要对类成员进行注释,而类成员也就是我们上面提到的常见数据类型。
其他
可以使用|操作符实现类似枚举值的效果,这对代码提示很友好
/**
* 捕获错误
* @param {object} status 错误代码
* @param { 1 | 2 | 3 | 'unknown'} status.code 错误代码。1 请求错误 2. 代码错误 3. 环境错误 4. 位置错误
*/
function handleCatch (status) {
if (status.code === 1) {
status.code
}
}
未定义的属性也可以预先写明对象的类型。 &符合可以用来合并对象。
/**
* 预置类型
* @type {{x: number} & {y: string}}
*/
let someVal = {}
// someVal.y
也支持不定数量参数的注释。
/**
* 不定数量参数
* @param {...number} arg
*/
function candyCount (...arg) {
}
// candyCount(2, 4)
生成文档
上面我们介绍了如何给各种各样的代码书写标准的规范,但是写完之后呢?只是为了在代码中看吗?当然不是。
如果注释足够规范,是可以一键生成对应的代码文档的。是的,你无需在手动书写文档了!
我们先看一下,将「注释生成文档」贯彻的最彻底的lodash是怎么做的。
lodash
/**
* This method is like `_.difference` except that it accepts `iteratee` which
* is invoked for each element of `array` and `values` to generate the criterion
* by which they're compared. The order and references of result values are
* determined by the first array. The iteratee is invoked with one argument:
* (value).
*
* **Note:** Unlike `_.pullAllBy`, this method returns a new array.
*
* @static
* @memberOf _
* @since 4.0.0
* @category Array
* @param {Array} array The array to inspect.
* @param {...Array} [values] The values to exclude.
* @param {Function} [iteratee=_.identity] The iteratee invoked per element.
* @returns {Array} Returns the new array of filtered values.
* @example
*
* _.differenceBy([2.1, 1.2], [2.3, 3.4], Math.floor);
* // => [1.2]
*
* // The `_.property` iteratee shorthand.
* _.differenceBy([{ 'x': 2 }, { 'x': 1 }], [{ 'x': 1 }], 'x');
* // => [{ 'x': 2 }]
*/
var differenceBy = baseRest(function(array, values) {
var iteratee = last(values);
if (isArrayLikeObject(iteratee)) {
iteratee = undefined;
}
return isArrayLikeObject(array)
? baseDifference(array, baseFlatten(values, 1, isArrayLikeObject, true), getIteratee(iteratee, 2))
: [];
});
我们可以看到文档中所有的内容与注释中的每一条都是一一对应。lodash在打包的时候,使用了docdown将注释转成了markdown文件,然后用这些markdown文件生成了html。
附录
下面附录列出了所有JSDoc提供的标签,我都做了简要的说明。常见标签我做了高亮标识。
| 标签名 | 使用范围 | 说明 |
|---|---|---|
| @abstract | 类 | 父类上定义抽象属性,由子类来实现 |
| @access | 文档 | @access package as @package@access private as @private@access protected as @protected@access public as @public |
| @alias | 文档 | 标识A为B的昵称 |
| @async | 函数 | 标识函数为异步函数,默认情况下无需使用,会自动识别。但可以为不在代码中声明的函数做虚拟注释是使用(如标识其他地方引入的$.ajax为异步函数) |
| @augments | 类 | 标识当前类的父类,同@extends |
| @author | 文档 | 作者名 |
| @borrows | 文档 | 复制另一个注释的所有信息 |
| @callback | 函数 | 标识一个回调函数,使用与@typedef 类似 |
| @class | 函数 | 标识一个函数为类 |
| @classdesc | 函数 | 类的描述,与@class搭配使用 |
| @constant | 常量 | 标识一个常量,会自动识别类型。 |
| @constructs | 对象字面量上的函数属性 | 一般配合@lends使用,标识一个函数为类的constructor |
| @copyright | 文档 | 版权信息 |
| @default | 值引用变量 | 为单个值引用变量在文档中生成默认值 |
| @deprecated | 文档 | 标识已废弃 |
| @description | 文档 | 描述。等同于注释最开始的描述,此标签优先级更高 |
| @enum | 文档 | 标识一个同类型集合 |
| @event | 类 | 标识一个类中的事件 |
| @example | 文档 | 可以用文本,或者代码写一个示例 |
| @exports | 文档 | 在非ES Module的模块规范下,标识导出的内容 |
| @external | 标识一个外部的对象,或者库。比如定义$为jQuery | |
| @file | 文档 | 一般用在文件开始,用来描述文件内容 |
| @fires | 类 | 标识在某个方法中会触发某个事件 |
| @function | 文档 | 标识一个函数 |
| @generator | 文档 | 标识一个生成器函数 (generator function) |
| @global | 标识一个属性为全局属性,如标识一个局部属性为全局属性 | |
| @hideconstructor | 文档 | 在文档中隐藏类的构造器函数 |
| @ignore | 文档 | 标识某个对象、属性不在文档中展示 |
| @implements | 标识类实现了哪个接口 | |
| @inheritdoc | 类,文档 | 标识子类继承父类的文档 |
| @inner | 标记某属性为类、模块的内部属性 | |
| @instance | 标识为当前实例专属属性 | |
| @interface | 标识一个可被实现的接口,接口可以只有注释,没有实体 | |
| @kind | 标识一个对象的类型 | |
| @lends | 对象字面量 | 标识一个对象字面量为一个类(使用某个函数生成类,对象字面量为其参数) |
| @license | 文档 | 软件许可证 |
| @listens | 标识监听了某个事件 | |
| @member | 标识某个属性是对象的属性 | |
| @memberof | 标识某个属性属于某个对象 | |
| @mixes | 标识当前对象混入了另一个对象 | |
| @mixin | 标识一个混入对象 | |
| @module | 标记当前文件为一个模块 | |
| @name | 覆盖命名一个变量。最好用在不在当前代码中声明的变量,也就是虚拟注释。 | |
| @namespace | 标识一个命名空间 | |
| @override | 子类覆盖父类的同名属性 | |
| @package | 文档 | package-private。即标识某个属性仅同目录可用。仅标识在输出文档。 |
| @param | 函数 | 函数入参 |
| @private | 类 | 私有。与普通的属性不同,被标识的属性不会出现在输出文档中。 |
| @property | 给对象的某个属性添加标识,配合@typedef使用,可生成任意自定义对象 | |
| @protected | 类 | 标识为仅本模块可用。 |
| @public | 类 | 所有属性默认即为public。仅标识,如果需要标识范围,需使用@instance, @static, and @global |
| @readonly | 文档 | 标记一个只读属性 |
| @requires | 标记当前模块依赖的模块,可以有多个 | |
| @returns | 函数 | 函数的返回值标识 |
| @see | 文档 | 链接到某处 |
| @since | 文档 | 从某个版本开始支持 |
| @static | 类 | 标识不会被实例化的类属性;或者模块中的某个属性。 |
| @summary | 文档 | @description 的简写版本 |
| @this | 标识当前作用域中this的指向 | |
| @throws | 函数 | 标识一个函数可能抛出的错误类型,可以有多个 |
| @todo | 文档 | 记录一些待实现的功能,可以有多个 |
| @tutorial | 文档 | 类似锚点,指向文档中定义的锚点 |
| @type | 标识一个类型 | |
| @typedef | 自定义类型。 | |
| @variation | 如果出现了重名,例如命名空间和类,用此标识来区分 | |
| @version | 文档 | 标识版本 |
| @yields | 标识生成器函数的返回值 |