你不知道的JSDoc

7,546 阅读8分钟

概要

你可能在代码中见过各种稀奇古怪的注释,丰富类型堪比各种稀奇古怪的代码。其实,无论是代码或者注释,都是有规范的,无论是官方的还是团队内定义的。我今天要介绍的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标识生成器函数的返回值

参考

jsdoc.app/index.html

github.com/google/clos…

youtrack.jetbrains.com/issue/WEB-5…

github.com/google/clos…

github.com/microsoft/v…