最早接触 JSDoc 是 2016 年领导要求给项目写 API 文档,最近一周被迫写 JavaScript 而不能写 TypeScript,又苦于 VS Code 的代码分析类型检查能力太弱,故祭出了 JSDoc 类型标注大法。
JSDoc 是 JavaScript 的文档生成工具,在 JavaScript 块注释的基础上提供了一套 tag 语法,如 @param {string} message
,可以根据源码中的这些特殊格式注释生成 API 文档。其他编程语言中也有类似的注释,这种注释块(Comment Block)也被称为 Docblock (DocBlock)。
除了用于生成 API 文档,JSDoc 另一个功能是标记变量、入参、返回值的类型,稍微弥补了 JavaScript 没有静态类型,不支持类型标注的问题。基于 JSDoc 的类型注释,Google Closure Compiler 可以分析优化代码,VS Code 之类的代码编辑器可以在不使用 TypeScript 的情况下实现代码提示。
历史
Michael Mathews 在 2001 年创建 JSDoc 项目,它是最早的 JavaScript 文档工具,注释语法借鉴自 Javadoc(很多文档工具都使用了类似 Javadoc 注释语法,比如 C++ 的 Doxygen、 PHP 的 phpDocumentor)。截止 2020 年经历了 4 个大版本更新:
- 2001 JSDoc (JSDoc.pm): 使用 Perl 编写,代码托管在 SourceForge
- 2007 JsDoc Toolkit 1.0: 使用 JavaScript 编写,基于 Rhino,运行在 Java 平台,代码托管在 Google Code
- 2008 JsDoc Toolkit 2.0: 同 1.0
- 2011 JSDoc 3: 基于 Node.js,代码托管在 GitHub
类型标注
JSDoc 要求块注释以 /**
开头,在 /*
、/***
开头的块注释,或者行注释 //
中使用会被忽略。
受编辑器(VS Code)限制,日常使用时,很少会使用到所有 JSDoc 支持的类型标注,另外像 Google Closure Compiler 之类的项目还基于 ECMAScript 4 草案实现了类型系统,定义了更丰富的类型标注。 这里主要介绍 TypeScript 支持的 JSDoc 标注语法,包含一部分常用的文档型标注。
涉及以下标注:
- 类型
@type
@typedef
@property
/@prop
@template
@enum
- 函数
@param
/@argument
/@arg
@returns
/@return
@throws
@deprecated
- 类和继承
@constructor
/@class
@this
@extends
@public
@private
@protected
@readonly
- 文档
@fileoverview
@author
@example
@description
@see
@link
@license
@preserve
定义文件头和作者
/**
* @fileoverview Manages the configuration settings for the widget.
* @author Rowina Sanela
*/
/**
* @author Jane Smith <jsmith@example.com>
*/
function MyClass() {}
定义链接
// Use the inline {@link} tag to include a link within a free-form description.
/**
* @see {@link foo} for further information.
* @see {@link http://github.com|GitHub}
*/
function bar() {}
/**
* {@link namepathOrURL}
* [link text]{@link namepathOrURL}
* {@link namepathOrURL|link text}
* {@link namepathOrURL link text (after the first space)}
*/
定义软件协议
/**
* Utility functions for the foo package.
* @license Apache-2.0
*/
/**
* @license
* Copyright (c) 2015 Example Corporation Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
定义使用示例
/**
* Solves equations of the form a * x = b
* @example
* // returns 2
* globalNS.method1(5, 10);
* @example
* // returns 3
* globalNS.method(5, 15);
* @returns {Number} Returns the value of x for the equation.
*/
globalNS.method1 = function (a, b) {
return b / a;
};
定义废弃的方法、属性
/**
* @deprecated since version 2.0
*/
function old() {
}
定义希望 minifier 保留的注释
/**
* @preserve Copyright 2009 SomeThirdParty.
* Here is the full license text and copyright
* notice for this file. Note that the notice can span several
* lines and is only terminated by the closing star and slash:
*/
定义单个类型
/**
* @type {string}
*/
var s;
/** @type {Window} */
var win;
/** @type {PromiseLike<string>} */
var promisedString;
/** @type {HTMLElement} */
var myElement = document.querySelector(selector);
element.dataset.myData = "";
/**
* @type {HTMLCollectionOf<HTMLElement>}
*/
const nodes = document.getElementsByClassName("className");
定义多个类型(联合类型,Union Type)
/**
* @type {(string | boolean)}
*/
var sb;
// TypeScript 中,圆括号可以省略
/**
* @type {string | boolean}
*/
var sb;
定义数组
/** @type {number[]} */
var ns;
/** @type {Array.<number>} */
var nds;
/** @type {Array<number>} */
var nas;
定义对象
/** @type {{ a: string, b: number }} */
var var9;
/**
* A map-like object that maps arbitrary `string` properties to `number`s.
*
* @type {Object.<string, number>}
*/
var stringToNumber;
/** @type {Object.<number, object>} */
var arrayLike;
定义函数
/** @type {function(string, boolean): number} Closure syntax */
var sbn;
/** @type {(s: string, b: boolean) => number} TypeScript syntax */
var sbn2;
/** @type {Function} */
var fn7;
/** @type {function} */
var fn6;
/** @typedef {{ prop1: string, prop2: string, prop3?: number }} SpecialType */
/** @typedef {(data: string, index?: number) => boolean} Predicate */
/** @type {SpecialType} */
var specialTypeFn1;
/** @type {Predicate} */
var specialTypeFn2;
/**
* @param {Object} options - The shape is the same as SpecialType above
* @param {string} options.prop1
* @param {number} options.prop2
* @param {number=} options.prop3
* @param {number} [options.prop4]
* @param {number} [options.prop5=42]
*/
function special(options) {
return (options.prop4 || 1001) + options.prop5;
}
// Parameters may be declared in a variety of syntactic forms
/**
* @param {string} p1 - A string param.
* @param {string=} p2 - An optional param (Closure syntax)
* @param {string} [p3] - Another optional param (JSDoc syntax).
* @param {string} [p4="test"] - An optional param with a default value
* @return {string} This is the result
*/
function stringsStringStrings(p1, p2, p3, p4) {
// TODO
}
/**
* @return {PromiseLike<string>}
*/
function ps() {}
/**
* @returns {{ a: string, b: number }} - May use '@returns' as well as '@return'
*/
function ab() {}
var someObj = {
/**
* @param {string} param1 - Docs on property assignments work
*/
x: function (param1) {},
};
/**
* As do docs on variable assignments
* @return {Window}
*/
let someFunc = function () {};
/**
* And class methods
* @param {string} greeting The greeting to use
*/
Foo.prototype.sayHi = (greeting) => console.log("Hi!");
/**
* And arrow functions expressions
* @param {number} x - A multiplier
*/
let myArrow = (x) => x * x;
/**
* Which means it works for stateless function components in JSX too
* @param {{a: string, b: number}} test - Some param
*/
var sfc = (test) => <div>{test.a.charAt(0)}</div>;
/**
* A parameter can be a class constructor, using Closure syntax.
*
* @param {{new(...args: any[]): object}} C - The class to register
*/
function registerClass(C) {}
/**
* @param {...string} p1 - A 'rest' arg (array) of strings. (treated as 'any')
*/
function fn10(p1) {}
/**
* @param {...string} p1 - A 'rest' arg (array) of strings. (treated as 'any')
*/
function fn9(p1) {
return p1.join();
}
/**
* @throws {DOMException}
*/
DOMApplicationCache.prototype.swapCache = function() {
};
定义 Promise
只能定义 Promise
在 resolve
后的类型,暂不支持表示 reject
。
/**
* Returns the sum of a and b
* @param {number} a
* @param {number} b
* @returns {Promise} Promise object represents the sum of a and b
*/
function sumAsync(a, b) {
return new Promise(function(resolve, reject) {
resolve(a + b);
});
}
/**
* @return {PromiseLike<string>}
*/
function ps() {}
写成 async
函数,可以用 @throws
间接表示 reject
。
/**
* @returns {Promise<string>}
* @throws {Error}
*/
async function foo() {
}
定义 any 类型
/**
* @type {*} - can be 'any' type
*/
var star;
/**
* @type {?} - unknown type (same as 'any')
*/
var question;
定义复杂类型
/**
* @typedef {Object} SpecialType - creates a new type named 'SpecialType'
* @property {string} prop1 - a string property of SpecialType
* @property {number} prop2 - a number property of SpecialType
* @property {number=} prop3 - an optional number property of SpecialType
* @prop {number} [prop4] - an optional number property of SpecialType
* @prop {number} [prop5=42] - an optional number property of SpecialType with default
*/
/** @type {SpecialType} */
var specialTypeObject;
specialTypeObject.prop3;
/**
* @typedef {object} SpecialType1 - creates a new type named 'SpecialType'
* @property {string} prop1 - a string property of SpecialType
* @property {number} prop2 - a number property of SpecialType
* @property {number=} prop3 - an optional number property of SpecialType
*/
/** @type {SpecialType1} */
var specialTypeObject1;
可以从 TypeScript 中引入类型
// @filename: types.d.ts
export type Pet = {
name: string,
};
// @filename: main.js
/**
* @param { import("./types").Pet } p
*/
function walk(p) {
console.log(`Walking ${p.name}...`);
}
/**
* @typedef { import("./types").Pet } Pet
*/
/**
* @type {Pet}
*/
var myPet;
myPet.name;
/**
* @type {typeof import("./accounts").userAccount }
*/
var x = require("./accounts").userAccount;
/** @type {import('webpack').Configuration} */
module.exports = {};
定义范型
/**
* @template T
* @param {T} x - A generic parameter that flows through to the return type
* @return {T}
*/
function id(x) {
return x;
}
const a = id("string");
const b = id(123);
const c = id({});
/**
* @template T,U,V
* @template W,X
*/
/**
* @template {string} K - K must be a string or string literal
* @template {{ serious(): string }} Seriousalizable - must have a serious method
* @param {K} key
* @param {Seriousalizable} object
*/
function seriousalize(key, object) {
// ????
}
定义类(Class)和继承
class C {
/**
* @param {number} data
*/
constructor(data) {
// property types can be inferred
this.name = "foo";
// or set explicitly
/** @type {string | null} */
this.title = null;
// or simply annotated, if they're set elsewhere
/** @type {number} */
this.size;
this.initialize(data); // Should error, initializer expects a string
}
/**
* @param {string} s
*/
initialize = function (s) {
this.size = s.length;
};
}
var c = new C(0);
// C should only be called with new, but
// because it is JavaScript, this is allowed and
// considered an 'any'.
var result = C(1);
支持旧的构造函数写法
/**
* @constructor
* @param {number} data
*/
function C(data) {
// property types can be inferred
this.name = "foo";
// or set explicitly
/** @type {string | null} */
this.title = null;
// or simply annotated, if they're set elsewhere
/** @type {number} */
this.size;
this.initialize(data);
// Argument of type 'number' is not assignable to parameter of type 'string'.
}
/**
* @param {string} s
*/
C.prototype.initialize = function (s) {
this.size = s.length;
};
var c = new C(0);
c.size;
var result = C(1);
// Value of type 'typeof C' is not callable. Did you mean to include 'new'?
继承的写法
/**
* @template T
* @extends {Set<T>}
*/
class SortableSet extends Set {
// ...
}
访问修饰符(Access Modifiers)
class Car {
constructor() {
/** @private */
this.identifier = 100;
}
printIdentifier() {
console.log(this.identifier);
}
}
const c = new Car();
console.log(c.identifier);
// Property 'identifier' is private and only accessible within class 'Car'.
只读属性
class Car {
constructor() {
/** @readonly */
this.identifier = 100;
}
printIdentifier() {
console.log(this.identifier);
}
}
const c = new Car();
console.log(c.identifier);
定义 this
/**
* @this {HTMLElement}
* @param {*} e
*/
function callbackForLater(e) {
this.clientHeight = parseInt(e); // should be fine!
}
定义枚举类型
/** @enum {number} */
const JSDocState = {
BeginningOfLine: 0,
SawAsterisk: 1,
SavingComments: 2,
};
JSDocState.SawAsterisk;
/** @enum {function(number): number} */
const MathFuncs = {
add1: (n) => n + 1,
id: (n) => -n,
sub1: (n) => n - 1,
};
MathFuncs.add1;
定义可选属性或者(非)空类型
/**
* Use postfix question on the property name instead:
* @type {{ a: string, b?: number }}
*/
var right;
空类型(nullable type)
/**
* @type {?number}
* With strictNullChecks: true -- number | null
* With strictNullChecks: false -- number
*/
var nullable;
/**
* @type {number | null}
* With strictNullChecks: true -- number | null
* With strictNullChecks: false -- number
*/
var unionNullable;
非空类型(Non-nullable type)
/**
* @type {!number}
* Just has type number
*/
var normal;
类型转换(Type Casting)
/**
* @type {number | string}
*/
var numberOrString = Math.random() < 0.5 ? "hello" : 100;
var typeAssertedNumber = /** @type {number} */ (numberOrString);
吐槽
写注释是提高代码可读性(Code Readability)和项目可维护性的重要途径。奈何一部分程序员把“代码即注释”、“代码自解释(Self-Explanatory Code)”当作自己不写注释的理由,甚至声称“优秀的程序员不写注释”,这些想法实在是自欺欺人和迷之自信。以前端项目为例,但凡看过一眼 React/Vue/TypeScript 等热门项目代码,都不至于把“优秀的代码不用注释”作为自己的懒惰和不专业的借口。
附录1:JSDoc 相关技术时间表
JSDoc 的更迭见证了技术的革新变化,我们不妨再回顾下相关文档生成、类型检查工具的历史。
- 1996 Javadoc (JavaDoc): Java 文档工具,包含在 JDK 1.0 中
- 1997 Doxygen: C++ 文档工具
- 1997 Rhino: 基于 Java 的 JavaScript 引擎
- 1999 HeaderDoc: Apple 开源的文档生成器,支持 Javadoc 和 Doxygen 标签
- 2001 JSDoc (JSDoc.pm): 使用 Perl 编写
- 2002 phpDocumentor (phpdoc): PHP 文档工具
- 2006 Google Web Toolkit (GWT): 允许开发者使用 Java 开发前端应用,最终编译为 JavaScript
- 2007 JsDoc Toolkit 1.0: 使用 JavaScript 编写,基于 Rhino,运行在 Java 平台
- 2008 ext-doc: ExtJS 注释处理工具,基于 Java
- 2008 V8: Google 开源的 JavaScript 引擎
- 2009 Google Closure Compiler (GCC): JavaScript 编译器,包含在 Google Closure Tools(曾被 Google 用于开发 Gmail、Google Docs、Google Maps)中
- 2009 CoffeeScript: 由 Jeremy Ashkenas 开发,可以被转译为 JavaScript
- 2009 Docco: CoffeeScript 文档生成器
- 2009 Node.js: 基于 V8
- 2010 JSDuck: Sencha 框架(Ext JS 4、Sencha Touch)使用的 API 文档生成器,使用 Ruby 编写
- 2011 JSDoc 3: 基于 Node.js
- 2011 Traceur Compiler: Google 开源的另一款 JavaScript 编译器,支持将 ES6+ 语法转译成 ES5 语法
- 2011 Dart: Google 开源的编程语言,可以被转译为 JavaScript
- 2011 YUIDoc: JavaScript 文档工具,包含在 Yahoo YUI 项目中
- 2012 jsdox: JavaScript 文档生成器
- 2012 TypeScript: Microsoft 开源的编程语言,JavaScript 的超集
- 2013 React.PropTypes: 包含在 React 中,于 2013 年开源
- 2014 Babel (6to5): JavaScript 转译器,2015 年由 6to5 更名为 Babel
- 2014 Flow: Facebook 于 2014 年开源的静态类型检查器
- 2015 ESDoc: JavaScript 文档生成工具,支持 ES6+ 语法
- 2015 TypeDoc: TypeScript 文档生成器
- 2016 API Extractor (@microsoft/api-extractor): TypeScript 分析工具,可以导出 API Report、.d.ts 类型定义、API 文档
- 2018 TSDoc (AEDoc): 一份用于标准化 TypeScript 内文档注释的提案
在 Javadoc 诞生后发布的文档生成工具,注释语法多少都受到它的影响。在 Node.js 出现之前,JavaScript 工具链也大都基于 Java 平台的 Rhino JavaScript 引擎。
附录2:ECMAScript 时间表
JSDoc / Google Closure Compiler / TypeScript 等工具使用的类型标注和那份未发布的 ECMAScript 4 规范有着特殊的联系。
- 1999 ECMAScript 3.0
- 2007 ECMAScript 4 Draft
- 2008 ECMAScript 3.1 Draft -> ECMAScript 5.0
- 2009 ECMAScript 5.0
- 2011 ECMAscript 5.1
- 2013 ECMAScript 6 Draft
- 2015 ECMAscript 6 (ECMAScript 2015)