大话 JavaScript(Speaking JavaScript):第二十六章到第三十章

2024-01-12 09:21:04 浏览数 (1)

第四部分:提示,工具和库

原文:IV. Tips, Tools, and Libraries 译者:飞龙 协议:CC BY-NC-SA 4.0

本部分提供了使用 JavaScript 的技巧(最佳实践,高级技术和学习资源),并描述了一些重要的工具和库。

第二十六章:元代码风格指南

原文:26. A Meta Code Style Guide 译者:飞龙 协议:CC BY-NC-SA 4.0

JavaScript 有许多优秀的风格指南。因此,没有必要再写一个。相反,本章描述了元风格规则,并调查了现有的风格指南和已建立的最佳实践。它还提到了我喜欢的一些更有争议的做法。这个想法是为了补充现有的风格指南,而不是取代它们。

现有的风格指南

这些是我喜欢的风格指南:

  • Idiomatic.js:编写一致的、惯用的 JavaScript 的原则
  • Google JavaScript 风格指南
  • jQuery JavaScript 风格指南
  • Airbnb JavaScript 风格指南

此外,还有两个元风格指南:

  • GitHub 上的流行约定分析 GitHub 代码,找出最常用的编码约定。
  • JavaScript,获胜的风格检查了几种流行风格指南的大多数推荐。

一般提示

本节将涵盖一些一般的代码编写技巧。

代码应该一致

编写一致代码的两个重要规则。第一条规则是,如果你开始一个新项目,你应该想出一个风格,记录下来,并在任何地方都遵循它。团队越大,检查对风格的自动遵循就越重要,可以通过诸如 JSHint 之类的工具实现。在风格方面,有许多决定要做。其中大多数都有普遍认可的答案。其他必须根据项目定义。例如:

  • 有多少空格(括号后,语句之间等)
  • 缩进(例如,每级缩进多少空格)
  • 如何在哪里编写var语句

第二条规则是,如果你加入一个现有项目,你应该严格遵循它的规则(即使你不同意它们)。

代码应该易于理解

每个人都知道调试比一开始编写程序要困难两倍。因此,如果你在编写时越聪明,那么你将如何调试呢? ——Brian Kernighan

对于大多数代码,用于阅读的时间远远大于用于编写的时间。因此,使前者尽可能简单非常重要。以下是一些指导方针:

更短并不总是更好

有时写更多意味着事情实际上更容易阅读。让我们考虑两个例子。首先,熟悉的事物更容易理解。这意味着使用熟悉的、稍微更冗长的结构可能更可取。其次,人类读取标记,而不是字符。因此,redBalloonrdBlln更容易阅读。

好的代码就像教科书

大多数代码库都充满了新的想法和概念。这意味着如果你想要使用一个代码库,你需要学习这些想法和概念。与教科书相比,代码的额外挑战在于人们不会线性阅读它。他们会随时跳进来,应该能够大致理解发生了什么。代码库的三个部分有所帮助:

  • 代码应该解释*发生了什么;它应该是不言自明的。为了编写这样的代码,使用描述性标识符,并将长函数(或方法)分解为更小的子函数。如果这些函数足够小并且有意义的名称,你通常可以避免注释。
  • 注释应该解释为什么事情发生。如果你需要了解一个概念才能理解代码,你可以在标识符中包含该概念的名称,或者在注释中提到它。阅读代码的人可以查阅文档,了解更多关于该概念的信息。
  • 文档应填补代码和注释留下的空白。它应该告诉你如何开始使用代码库,并为你提供大局观。它还应包含所有重要概念的词汇表。

不要聪明;不要让我思考

有很多巧妙的代码利用对语言的深入了解来实现令人印象深刻的简洁性。这样的代码通常像一个谜题,很难理解。你会遇到这样的观点,如果人们不理解这样的代码,也许他们真的应该先学习 JavaScript。但这不是这篇文章要讨论的。无论你有多聪明,进入其他人的思维世界总是具有挑战性的。所以简单的代码并不愚蠢,它是大部分努力都花在让一切易于理解的代码。

避免为速度或代码大小进行优化

许多巧妙的技巧都是针对这些优化的。然而,你通常不需要它们。一方面,JavaScript 引擎变得越来越智能,自动优化遵循已建立模式的代码的速度。另一方面,缩小工具(第三十二章)重写你的代码,使其尽可能小。在这两种情况下,工具都是为你聪明的,这样你就不必自己聪明了。

有时你别无选择,只能优化代码的性能。如果你这样做,请确保测量和优化正确的部分。在浏览器中,问题通常与 DOM 和 HTML 相关,而不是语言本身。

常见的最佳实践

大多数 JavaScript 程序员都同意以下最佳实践:

使用严格模式。它使 JavaScript 成为一种更清洁的语言(参见严格模式)。

始终使用分号。避免自动分号插入的陷阱(参见自动分号插入)。

始终使用严格相等(===)和严格不等(!==)。我建议永远不要偏离这个规则。即使它们是等价的,我甚至更喜欢以下两个条件中的第一个:

代码语言:javascript复制
if (x !== undefined && x !== null) ...  // my choice
if (x != null) ...  // equivalent

要么只使用空格,要么只使用制表符进行缩进,但不要混合使用它们。

引用字符串:在 JavaScript 中,你可以用单引号或双引号写字符串文字。单引号更常见。它们使得处理 HTML 代码更容易(通常 HTML 代码中的属性值是双引号)。其他考虑因素在字符串文字中提到。

避免全局变量(最佳实践:避免创建全局变量)。

括号样式

在大括号界定代码块的语言中,括号样式决定你放置这些括号的位置。在类 C 语言(如 Java 和 JavaScript)中,有两种最常见的括号样式:Allman 样式和 1TBS。

Allman 样式

如果一个语句包含一个块,那么该块被认为与语句的头部有些分离:它的左大括号在自己的一行上,与头部的缩进级别相同。例如:

代码语言:javascript复制
// Allman brace style
function foo(x, y, z)
{
    if (x)
    {
        a();
    }
    else
    {
        b();
        c();
    }
}
1TBS(真正的括号样式)

在这里,一个块与其语句的标题更紧密地关联在一起;它在同一行之后开始。控制流语句的主体总是放在大括号中,即使只有一个语句。例如:

代码语言:javascript复制
// One True Brace Style
function foo(x, y, z) {
    if (x) {
        a();
    } else {
        b();
        c();
    }
}

1TBS 是(Kernighan 和 Ritchie)样式的一个变体。在 K&R 样式中,函数以 Allman 样式编写,并且在不必要的情况下省略大括号,例如,在单语句then情况下:

代码语言:javascript复制
// K&R brace style
function foo(x, y, z)
{
    if (x)
        a();
    else {
        b();
        c();
    }
}
JavaScript

JavaScript 世界中的事实标准是 1TBS。它是从 Java 继承而来,大多数风格指南都推荐使用它。其中一个原因是客观的。如果你返回一个对象字面量,你必须将开括号放在与关键字return相同的行上,就像这样(否则,自动分号插入会在return后插入一个分号,意味着什么也没有返回;参见Pitfall: ASI can unexpectedly break up statements):

代码语言:javascript复制
return {
    name: 'Jane'
};

显然,对象字面量不是一个代码块,但如果两者格式化方式相同,看起来更一致,你犯错的可能性就更小。

我的个人风格和偏好是:

1TBS(这意味着你应该尽可能使用大括号)。

作为例外,如果一个语句可以写在一行上,我会省略大括号。例如:

代码语言:javascript复制
if (x) return x;
更喜欢字面量而不是构造函数

几个字面量产生的对象也可以通过构造函数创建。然而,后者通常是更好的选择:

代码语言:javascript复制
var obj = new Object(); // no
var obj = {}; // yes

var arr = new Array(); // no
var arr = []; // yes

var regex = new RegExp('abc'); // avoid if possible
var regex = /abc/; // yes

永远不要使用构造函数Array来创建具有给定元素的数组。初始化具有元素的数组(避免!)解释了原因:

代码语言:javascript复制
var arr = new Array('a', 'b', 'c'); // never ever
var arr = [ 'a', 'b', 'c' ]; // yes
不要聪明

本节收集了一些不推荐的聪明用法。

条件运算符

不要嵌套条件运算符:

代码语言:javascript复制
// Don’t:
return x === 0 ? 'red' : x === 1 ? 'green' : 'blue';

// Better:
if (x === 0) {
    return 'red';
} else if (x === 1) {
    return 'green';
} else {
    return 'blue';
}

// Best:
switch (x) {
    case 0:
        return 'red';
    case 1:
        return 'green';
    default:
        return 'blue';
}
缩写 if 语句

不要通过逻辑运算符缩写if语句:

代码语言:javascript复制
foo && bar(); // no
if (foo) bar(); // yes

foo || bar(); // no
if (!foo) bar(); // yes
增量运算符

如果可能的话,使用增量运算符( )和减量运算符(--)作为语句;不要将它们用作表达式。在后一种情况下,它们会返回一个值,虽然有一个助记符,但你仍然需要思考来弄清楚发生了什么:

代码语言:javascript复制
// Unsure: what is happening?
return   foo;

// Easy to understand
  foo;
return foo;
检查未定义
代码语言:javascript复制
if (x === void 0) x = 0; // not necessary in ES5
if (x === undefined) x = 0; // preferable

从 ECMAScript 5 开始,第二种检查方式更好。更改未定义解释了为什么。

将数字转换为整数
代码语言:javascript复制
return x >> 0; // no
return Math.round(x); // yes

移位运算符可以用来将数字转换为整数。然而,通常最好使用更明确的替代方法,比如Math.round()。转换为整数概述了所有转换为整数的方法。

可接受的聪明用法

有时候你可以在 JavaScript 中很聪明——如果这种聪明已经成为一种已经建立的模式。

默认值

使用或(||)运算符提供默认值是一种常见的模式——例如,对于参数:

代码语言:javascript复制
function f(x) {
    x = x || 0;
    ...
}

有关详细信息和更多示例,请参阅模式:提供默认值。

通用方法

如果你通用地使用方法,你可以将Object.prototype缩写为{}。以下两个表达式是等价的:

代码语言:javascript复制
Object.prototype.hasOwnProperty.call(obj, propKey)
{}.hasOwnProperty.call(obj, propKey)

Array.prototype可以缩写为[]

代码语言:javascript复制
Array.prototype.slice.call(arguments)
[].slice.call(arguments)

我对这个持观望态度。这是一个技巧(你正在通过一个实例访问原型属性)。但它减少了混乱,我期望引擎最终会优化这种模式。

ECMAScript 5:尾随逗号

在 ECMAScript 5 中,对象字面量中的尾随逗号是合法的:

代码语言:javascript复制
var obj = {
    first: 'Jane',
    last: 'Doe', // legal: trailing comma
};
ECMAScript 5:保留字

ECMAScript 5 还允许你使用保留字(如new)作为属性键:

代码语言:javascript复制
> var obj = { new: 'abc' };
> obj.new
'abc'

有争议的规则

让我们看看一些我喜欢的、有点具有争议的惯例。

语法

我们将从语法惯例开始:

紧凑的空格

我喜欢相对紧凑的空格。这个模型是用英语写的:在开括号后和闭括号前没有空格。逗号后有空格:

代码语言:javascript复制
var result = foo('a', 'b');
var arr = [ 1, 2, 3 ];
if (flag) {
    ...
}

对于匿名函数,我遵循道格拉斯·克罗克福德的规则,在关键字function后面加一个空格。理由是,如果去掉名字,这就是一个命名函数表达式的样子:

代码语言:javascript复制
function foo(arg) { ... }  // named function expression
function (arg) { ... }     // anonymous function expression

每个缩进级别四个空格

我看到的大多数代码都使用空格缩进,因为制表符在应用程序和操作系统之间显示的方式有很大不同。我更喜欢每级缩进四个空格,因为这样缩进更加明显。

将条件操作符放在括号中

这有助于阅读,因为更容易确定操作符的范围:

代码语言:javascript复制
return result ? result : theDefault;  // no
return (result ? result : theDefault);  // yes
变量

接下来,我将介绍变量的约定:

每行只声明一个变量

不要用单个声明声明多个变量:

代码语言:javascript复制
// no
var foo = 3,
    bar = 2,
    baz;

// yes
var foo = 3;
var bar = 2;
var baz;

这种方法的优势在于删除、插入和重新排列行更简单,行也会自动正确缩进。

保持变量声明局部

如果你的函数不太长(无论如何都不应该太长),那么你可以在提升方面放松一些,假装var声明是块作用域的。换句话说,你可以在使用变量的上下文中声明变量(在循环内,在then块或else块内等)。这种局部封装使得代码片段更容易理解。也更容易删除代码片段或将其移动到其他地方。

如果你在一个块内,就待在那个块内

作为前一条规则的补充:不要在两个不同的块中声明相同的变量。例如:

代码语言:javascript复制
// Don’t do this
if (v) {
    var x = v;
} else {
    var x = 10;
}
doSomethingWith(x);

前面的代码和下面的代码有相同的效果和意图,所以应该这样写:

代码语言:javascript复制
var x;
if (v) {
    x = v;
} else {
    x = 10;
}
doSomethingWith(x);
面向对象

现在我们将讨论与面向对象有关的约定。

优先使用构造函数而不是其他实例创建模式

我建议你:

  • 总是使用构造函数。
  • 创建实例时总是使用new

这样做的主要优势是:

  • 你的代码更适合 JavaScript 主流,更有可能在不同框架之间移植。
  • 在现代引擎中,使用构造函数的实例非常快(例如,通过hidden classes)。
  • 在即将到来的 ECMAScript 6 中,类将是默认的继承构造。

对于构造函数,使用严格模式很重要,因为它可以防止你忘记实例化时使用new操作符。你应该知道你可以在构造函数中返回任何对象。有关使用构造函数的更多提示,请参阅实现构造函数的提示。

避免使用闭包来处理私有数据

如果你希望对象的私有数据完全安全,你必须使用闭包。否则,你可以使用普通属性。一个常见的做法是在私有属性的名称前加下划线。闭包的问题在于代码变得更加复杂(除非你将所有方法都放在实例中,这是不符合惯例且慢的),而且速度更慢(访问闭包中的数据目前比访问属性更慢)。保持数据私有更详细地介绍了这个主题。

如果构造函数没有参数,写括号

我发现这样的构造函数调用用括号看起来更清晰:

代码语言:javascript复制
var foo = new Foo;  // no
var foo = new Foo();  // yes

小心操作符优先级

使用括号,这样两个操作符就不会相互竞争——结果并不总是你所期望的:

代码语言:javascript复制
> false && true || true
true
> false && (true || true)
false
> (false && true) || true
true

instanceof特别棘手:

代码语言:javascript复制
> ! {} instanceof Array
false
> (!{}) instanceof Array
false
> !({} instanceof Array)
true

然而,我发现构造函数后的方法调用并不成问题:

代码语言:javascript复制
new Foo().bar().baz();  // ok
(new Foo()).bar().baz();  // not necessary
杂项

这一部分收集了各种提示:

强制转换

通过BooleanNumberString()Object()(作为函数使用——永远不要将这些函数用作构造函数)将值强制转换为类型。理由是这种约定更具描述性:

代码语言:javascript复制
>  '123'  // no
123
> Number('123')  // yes
123

> '' true  // no
'true'
> String(true)  // yes
'true'

避免使用this作为隐式参数

this应该只指当前方法调用的接收者;不应滥用作为隐式参数。理由是这样的函数更容易调用和理解。我也喜欢保持面向对象和函数机制分开:

代码语言:javascript复制
// Avoid:
function handler() {
    this.logError(...);
}

// Prefer:
function handler(context) {
    context.logError(...);
}

通过inhasOwnProperty检查属性的存在(参见Iteration and Detection of Properties)

这比与undefined进行比较或检查真实性更加自明和安全:

代码语言:javascript复制
// All properties:
if (obj.foo)  // no
if (obj.foo !== undefined)  // no
if ('foo' in obj) ... // yes

// Own properties:
if (obj.hasOwnProperty('foo')) ... // risky for arbitrary objects
if (Object.prototype.hasOwnProperty.call(obj, 'foo')) ... // safe

快速失败

如果可以的话,最好是快速失败,而不是悄悄失败。JavaScript 只是如此宽容(例如,除以零),因为 ECMAScript 的第一个版本没有异常。例如,不要强制转换值;抛出异常。但是,当您的代码处于生产状态时,您必须找到从失败中恢复的方法。

结论

每当您考虑样式问题时,请问自己:什么使我的代码更容易理解?抵制诱惑,不要聪明,把大部分机械聪明留给 JavaScript 引擎和缩小器(参见第三十二章)。


¹⁹ 甚至有人说它们是同义词,1TBS 是一种戏谑地指代 K&R 的方式。

第二十七章:调试的语言机制

原文:27. Language Mechanisms for Debugging 译者:飞龙 协议:CC BY-NC-SA 4.0

以下三种语言结构有助于调试。它们显然应该由适当的调试器补充:

  • debugger语句的行为类似于断点,并启动调试器。
  • console.log(x)将值x记录到 JavaScript 引擎的控制台中。
  • console.trace()将堆栈跟踪打印到引擎的控制台中。

控制台 API 提供了更多的调试帮助,并在The Console API中有更详细的文档。异常处理在第十四章中有解释。

第二十八章:内置子类

原文:28. Subclassing Built-ins 译者:飞龙 协议:CC BY-NC-SA 4.0

JavaScript 的内置构造函数很难进行子类化。本章解释了原因并提出了解决方案。

术语

我们使用短语subclass a built-in并避免术语extend,因为它在 JavaScript 中被使用:

子类化内置A

创建给定内置构造函数A的子构造函数BB的实例也是A的实例。

扩展对象obj

将一个对象的属性复制到另一个对象。Underscore.js 使用这个术语,延续了 Prototype 框架建立的传统。

子类化内置有两个障碍:具有内部属性的实例和无法作为函数调用的构造函数。

障碍 1:具有内部属性的实例

大多数内置构造函数都有所谓的内部属性的实例(参见Kinds of Properties),它们的名称用双方括号写成,像这样:[[PrimitiveValue]]。内部属性由 JavaScript 引擎管理,通常在 JavaScript 中无法直接访问。JavaScript 中的正常子类化技术是使用this的子构造函数调用超级构造函数(参见Layer 4: Inheritance Between Constructors):

代码语言:javascript复制
function Super(x, y) {
    this.x = x;  // (1)
    this.y = y;  // (1)
}
function Sub(x, y, z) {
    // Add superproperties to subinstance
    Super.call(this, x, y);  // (2)
    // Add subproperty
    this.z = z;
}

大多数内置忽略作为this传入的子实例(2),这是下一节描述的一个障碍。此外,向现有实例添加内部属性(1)通常是不可能的,因为它们往往会从根本上改变实例的性质。因此,不能使用(2)处的调用来添加内部属性。以下构造函数具有具有内部属性的实例:

包装构造函数

BooleanNumberString的实例包装原始值。它们都有内部属性[[PrimitiveValue]],其值由valueOf()返回;String还有两个额外的实例属性:

  • Boolean:内部实例属性[[PrimitiveValue]]
  • Number:内部实例属性[[PrimitiveValue]]
  • String:内部实例属性[[PrimitiveValue]],自定义内部实例方法[[GetOwnProperty]],普通实例属性length[[GetOwnProperty]]使得可以通过使用数组索引时从包装字符串中读取字符来进行索引访问。

Array

自定义的内部实例方法[[DefineOwnProperty]]拦截正在设置的属性。它确保length属性正常工作,通过在添加数组元素时保持length的最新状态,并在length变小时删除多余的元素。

Date

内部实例属性[[PrimitiveValue]]存储由日期实例表示的时间(自 1970 年 1 月 1 日 00:00:00 UTC 以来的毫秒数)。

Function

内部实例属性[[Call]](实例被调用时要执行的代码)和可能还有其他属性。

RegExp

内部实例属性[[Match]],以及两个非内部实例属性。来自 ECMAScript 规范:

[[Match]]内部属性的值是RegExp对象的模式的实现相关表示。

唯一没有内部属性的内置构造函数是ErrorObject

解决障碍 1

MyArrayArray的子类。它有一个 getter size,返回数组中的实际元素,忽略了空洞(其中length考虑了空洞)。实现MyArray的技巧是创建一个数组实例,并将其方法复制到其中:²⁰

代码语言:javascript复制
function MyArray(/*arguments*/) {
    var arr = [];
    // Don’t use Array constructor to set up elements (doesn’t always work)
    Array.prototype.push.apply(arr, arguments);  // (1)
    copyOwnPropertiesFrom(arr, MyArray.methods);
    return arr;
}
MyArray.methods = {
    get size() {
        var size = 0;
        for (var i=0; i < this.length; i  ) {
            if (i in this) size  ;
        }
        return size;
    }
}

此代码使用辅助函数copyOwnPropertiesFrom(),该函数在Copying an Object中显示和解释。

我们在第(1)行不调用Array构造函数,因为有一个怪癖:如果它以一个数字作为单个参数调用,那么这个数字不会成为一个元素,而是确定一个空数组的长度(参见Initializing an array with elements (avoid!))。

以下是交互:

代码语言:javascript复制
> var a = new MyArray('a', 'b')
> a.length = 4;
> a.length
4
> a.size
2
注意事项

将方法复制到实例会导致冗余,如果可以使用原型,则可以避免这种情况。此外,MyArray创建的对象不是其实例:

代码语言:javascript复制
> a instanceof MyArray
false
> a instanceof Array
true

障碍 2:无法将构造函数作为函数调用

即使Error和子类没有具有内部属性的实例,您仍然无法轻松地对其进行子类化,因为子类化的标准模式不起作用(与之前重复):

代码语言:javascript复制
function Super(x, y) {
    this.x = x;
    this.y = y;
}
function Sub(x, y, z) {
    // Add superproperties to subinstance
    Super.call(this, x, y);  // (1)
    // Add subproperty
    this.z = z;
}

问题在于Error总是产生一个新实例,即使作为函数调用(1);也就是说,它忽略了通过call()传递给它的this参数:

代码语言:javascript复制
> var e = {};
> Object.getOwnPropertyNames(Error.call(e)) // new instance
[ 'stack', 'arguments', 'type' ]
> Object.getOwnPropertyNames(e) // unchanged
[]

在前面的交互中,Error返回了一个具有自己属性的实例,但它是一个新实例,而不是e。如果Error将自己的属性添加到this(在前面的情况下是e),那么子类化模式将起作用。

解决障碍 2

在子构造函数内部,创建一个新的超级实例,并将其自己的属性复制到子实例中:

代码语言:javascript复制
function MyError() {
    // Use Error as a function
    var superInstance = Error.apply(null, arguments);
    copyOwnPropertiesFrom(this, superInstance);
}
MyError.prototype = Object.create(Error.prototype);
MyError.prototype.constructor = MyError;

辅助函数copyOwnPropertiesFrom()在Copying an Object中显示。尝试MyError

代码语言:javascript复制
try {
    throw new MyError('Something happened');
} catch (e) {
    console.log('Properties: ' Object.getOwnPropertyNames(e));
}

这是在 Node.js 上的输出:

代码语言:javascript复制
Properties: stack,arguments,message,type

instanceof关系应该是正常的:

代码语言:javascript复制
> new MyError() instanceof Error
true
> new MyError() instanceof MyError
true

另一种解决方案:委托

委托是子类化的一个非常干净的替代方法。例如,要创建自己的数组构造函数,您可以在属性中保留一个数组:

代码语言:javascript复制
function MyArray(/*arguments*/) {
    this.array = [];
    Array.prototype.push.apply(this.array, arguments);
}
Object.defineProperties(MyArray.prototype, {
    size: {
        get: function () {
            var size = 0;
            for (var i=0; i < this.array.length; i  ) {
                if (i in this.array) size  ;
            }
            return size;
        }
    },
    length: {
        get: function () {
            return this.array.length;
        },
        set: function (value) {
            return this.array.length = value;
        }
    }
});

显而易见的限制是,您无法通过方括号访问MyArray的元素;您必须使用方法来这样做:

代码语言:javascript复制
MyArray.prototype.get = function (index) {
    return this.array[index];
}
MyArray.prototype.set = function (index, value) {
    return this.array[index] = value;
}

可以通过以下元编程的方法传递Array.prototype的普通方法:

代码语言:javascript复制
[ 'toString', 'push', 'pop' ].forEach(function (key) {
    MyArray.prototype[key] = function () {
        return Array.prototype[key].apply(this.array, arguments);
    }
});

我们通过在存储在MyArray实例中的数组this.array上调用它们,从Array方法派生MyArray方法。

使用MyArray

代码语言:javascript复制
> var a = new MyArray('a', 'b');
> a.length = 4;
> a.push('c')
5
> a.length
5
> a.size
3
> a.set(0, 'x');
> a.toString()
'x,b,,,c'

²⁰受Ben Nadel的一篇博文的启发。

第二十九章:JSDoc:生成 API 文档

原文:29. JSDoc: Generating API Documentation 译者:飞龙 协议:CC BY-NC-SA 4.0

这是一个常见的开发问题:您已经编写了 JavaScript 代码,其他人需要使用它,并且需要一个漂亮的 HTML 文档来描述其 API。在 JavaScript 世界中生成 API 文档的事实标准工具是JSDoc。²¹ 它是模仿其 Java 模拟品 JavaDoc 而建立的。

JSDoc 接受带有/** */注释的 JavaScript 代码(以星号开头的普通块注释)并为其生成 HTML 文档。例如,给定以下代码:

代码语言:javascript复制
/** @namespace */
var util = {
    /**
 * Repeat <tt>str</tt> several times.
 * @param {string} str The string to repeat.
 * @param {number} [times=1] How many times to repeat the string.
 * @returns {string}
 */
    repeat: function(str, times) {
        if (times === undefined || times < 1) {
            times = 1;
        }
        return new Array(times 1).join(str);
    }
};

生成的 HTML 在 Web 浏览器中显示如图 29-1所示。

图 29-1. JSDoc 生成的 HTML 输出。

JSDoc 网站上的自述文件解释了如何安装和调用这个工具。

JSDoc 的基础知识

JSDoc 的全部内容都是关于文档化实体(函数、方法、构造函数等)。这是通过在实体之前的注释中实现的,这些注释以/**开头。

语法

让我们回顾一下开头显示的注释:

代码语言:javascript复制
/**
 * Repeat <tt>str</tt> several times.
 * @param {string} str The string to repeat.
 * @param {number} [times=1] How many times to repeat the string.
 * @returns {string}
 */

这演示了一些 JSDoc 的语法,包括以下部分:

JSDoc 注释

这是一个 JavaScript 块注释,其第一个字符是星号。这会产生一个假象,即/**标记开始了这样的注释。

标签

您可以通过以@符号为前缀的标签开始行来构造注释。在前面的代码中,@param就是一个例子。

HTML

您可以在 JSDoc 注释中自由使用 HTML。例如,<tt>显示单词的等宽字体。

类型注释

您可以通过大括号中的类型名称来记录实体的类型。变化包括:

  • 单一类型:@param {string} name
  • 多种类型:@param {string|number} idCode
  • 类型为数组:@param {string[]} names

名称路径

在 JSDoc 注释中,所谓的namepaths用于引用实体。这些路径的语法如下:

代码语言:javascript复制
myFunction
MyClass
MyClass.staticMember
MyClass#instanceMember

通常(由)构造函数实现。静态成员是构造函数的属性。JSDoc 对实例成员有一个广泛的定义。它意味着可以通过实例访问的一切。因此,实例成员包括实例属性和原型属性。

命名类型

实体的类型要么是基本类型,要么是类。前者的名称总是以小写字母开头;后者的名称总是以大写字母开头。换句话说,基本类型的类型名称是booleannumberstring,就像typeof运算符返回的结果一样。这样,您就不会混淆字符串(基本类型)和构造函数String的实例(对象)。

基本标签

以下是基本的元数据标签:

@fileOverview description

标记描述整个文件的 JSDoc 注释。例如:

代码语言:javascript复制
/**
 * @fileOverview Various tool functions.
 * @author <a href="mailto:jd@example.com">John Doe</a>
 * @version 3.1.2
 */

@author

指的是谁编写了正在被记录的实体。

@deprecated

指示实体不再受支持。记录应该使用什么是一个很好的做法。

@example

包含一个代码示例,说明给定的实体应该如何使用:

代码语言:javascript复制
/**
 * @example
 * var str = 'abc';
 * console.log(repeat(str, 3)); // abcabcabc
 */

用于链接的基本标签如下:

@see

指向相关资源:

代码语言:javascript复制
/**
 * @see MyConstructor#myMethod
 * @see The <a href="http://example.com">Example Project</a>.
 */

{@link ...}

@see一样工作,但可以在其他标签内使用。

@requires resourceDescription

指示文档实体需要的资源。资源描述可以是名称路径或自然语言描述。

版本标签包括以下内容:

@version versionNumber

指示文档实体的版本。例如:

代码语言:javascript复制
@version 10.3.1

@since versionNumber

指示文档实体可用的版本。例如:

代码语言:javascript复制
@since 10.2.0

文档化函数和方法

对于函数和方法,您可以记录参数、返回值和可能抛出的异常:

@param {paramType} paramName description

描述了参数的名称为paramName。类型和描述是可选的。以下是一些例子:

代码语言:javascript复制
@param str The string to repeat.
@param {string} str
@param {string} str The string to repeat.

高级特性:

可选参数:

代码语言:javascript复制
@param {number} [times] The number of times is optional.

带有默认值的可选参数:

代码语言:javascript复制
@param {number} [times=1] The number of times is optional.

@returns {returnType} description

描述函数或方法的返回值。类型或描述可以省略。

@throws {exceptionType} description

描述在函数或方法执行过程中可能抛出的异常。类型或描述可以省略。

内联类型信息(“内联文档注释”)

为参数和返回值提供类型信息有两种方式。首先,您可以在@param@returns中添加类型注释:

代码语言:javascript复制
/**
 * @param {String} name
 * @returns {Object}
 */
function getPerson(name) {
}

其次,您可以内联类型信息:

代码语言:javascript复制
function getPerson(/**String*/ name) /**Object*/ {
}

记录变量、参数和实例属性

以下标签用于记录变量、参数和实例属性:

@type {typeName}

所记录的变量的类型是什么?例如:

代码语言:javascript复制
/** @type {number} */
var carCounter = 0;

此标签也可用于记录函数的返回类型,但在这种情况下,@returns更可取。

@constant

指示所记录的变量具有常量值的标志。

代码语言:javascript复制
/** @constant */
var FORD = 'Ford';

@property {propType} propKey description

在构造函数注释中记录实例属性。例如:

代码语言:javascript复制
/**
 * @constructor
 * @property {string} name The name of the person.
 */
function Person(name) {
    this.name = name;
}

另外,实例属性可以如下记录:

代码语言:javascript复制
/**
 * @class
 */
function Person(name) {
    /**
 * The name of the person.
 * @type {string}
 */
    this.name = name;
}

使用哪种风格取决于个人偏好。

@default defaultValue

参数或实例属性的默认值是什么?例如:

代码语言:javascript复制
/** @constructor */
function Page(title) {
    /**
 * @default 'Untitled'
 */
     this.title = title || 'Untitled';
}

记录类

JSDoc 区分类和构造函数。前者更像是一种类型,而构造函数是实现类的一种方式。JavaScript 内置的定义类的方法有限,这就是为什么有许多 API 来帮助完成这个任务。这些 API 有所不同,通常差异很大,因此您必须帮助 JSDoc 弄清楚发生了什么。以下标签让您可以做到这一点:

@constructor

将函数标记为构造函数。

@class

将变量或函数标记为类。在后一种情况下,@class@constructor的同义词。

@constructs

记录方法设置实例数据。如果存在这样的方法,则在该类中记录。

@lends namePath

指定以下对象文字贡献给哪个类。有两种贡献的方式。

  • @lends Person#:对象文字为Person贡献实例成员。
  • @lends Person:对象文字为Person贡献静态成员。

@memberof parentNamePath

所记录的实体是指定对象的成员。@lends MyClass#,应用于对象文字,与使用@memberof MyClass#标记该文字的每个属性具有相同的效果。

定义类最常见的方式是:通过构造函数、通过对象文字以及通过具有@constructs方法的对象文字。

通过构造函数定义类

要通过构造函数定义类,必须标记构造函数;否则,它将不会被文档化为类。仅仅大小写不足以标记函数为构造函数:

代码语言:javascript复制
/**
 * A class for managing persons.
 * @constructor
 */
function Person(name) {
}
通过对象文字定义类

要通过对象文字定义类,需要两个标记。首先,您需要告诉 JSDoc 给定的变量持有一个类。其次,您需要标记一个对象文字为定义类。您可以通过@lends标签来实现后者:

代码语言:javascript复制
/**
 * A class for managing persons.
 * @class
 */
var Person = makeClass(
    /** @lends Person# */
    {
        say: function(message) {
            return 'This person says: '   message;
        }
    }
);
通过具有@constructs 方法的对象文字定义类

如果对象文字有一个@constructs方法,您需要告诉 JSDoc 关于它,这样它才能找到实例属性的文档。类的文档移到该方法中:

代码语言:javascript复制
var Person = makeClass(
    /** @lends Person# */
    {
        /**
 * A class for managing persons.
 * @constructs
 */
        initialize: function(name) {
            this.name = name;
        },
        say: function(message) {
            return this.name   ' says: '   message;
        }
    }
);

如果省略@lends,则必须指定方法属于哪个类:

代码语言:javascript复制
var Person = makeClass({
        /**
 * A class for managing persons.
 * @constructs Person
 */
        initialize: function(name) {
            this.name = name;
        },
        /** @memberof Person# */
        say: function(message) {
            return this.name   ' says: '   message;
        }
    }
);
子类化

JavaScript 没有内置的子类化支持。当您在代码中进行子类化(无论是手动还是通过库),您必须告诉 JSDoc 发生了什么:

@extends namePath

指示所记录的类是另一个类的子类的标志。例如:

代码语言:javascript复制
/**
 * @constructor
 * @extends Person
 */
function Programmer(name) {
    Person.call(this, name);
    ...
}
// Remaining code for subclassing omitted

其他有用的标签

所有这些标签都在JSDoc 网站上有文档:

  • 模块化:@module@exports@namespace
  • 自定义类型(用于虚拟实体,如回调,其签名可以由您记录):@typedef@callback
  • 法律事务:@copyright@license
  • 各种对象:@mixin@enum

²¹ JSDoc 网站是本章的主要来源;其中一些示例是从该网站借用的。

第三十章:库

原文:30. Libraries 译者:飞龙 协议:CC BY-NC-SA 4.0

本章介绍了 JavaScript 库。首先解释了 shim 和 polyfill 是什么,这两种特殊的库。然后列出了一些核心库。最后,指向了其他与库相关的资源。

Shim 与 Polyfill 的区别

Shim 和 polyfill 是在旧的 JavaScript 引擎上改进新功能的库:

  • Shim是一个库,它将新的 API 引入到旧的环境中,只使用该环境的手段。
  • 填充物是浏览器 API 的替代品。它通常检查浏览器是否支持 API。如果不支持,填充物会安装自己的实现。这样你就可以在任何情况下使用 API。术语填充物来自于家居改进产品;根据Remy Sharp的说法:

Polyfilla 是一种在美国被称为 Spackling Paste 的英国产品。考虑到这一点:把浏览器想象成有裂缝的墙。这些[填充物]有助于填平裂缝,使我们得到一个漂亮平滑的浏览器墙来使用。

示例包括:

  • “HTML5 跨浏览器填充物”:由 Paul Irish 编制的列表。
  • es5-shim是一个(非填充物)shim,它在 ECMAScript 3 引擎上改进了 ECMAScript 5 的功能。它纯粹与语言相关,无论是在 Node.js 上还是在浏览器上都是有意义的。

四种语言库

以下库已经相当成熟并且接近语言。了解它们是有用的:

  • ECMAScript 国际化 API 有助于处理与国际化相关的任务:排序和搜索字符串、数字格式化以及日期和时间格式化。下一节将更详细地解释此 API。
  • Underscore.js通过数组、对象、函数等工具函数来补充 JavaScript 相对稀疏的标准库。由于 Underscore 早于 ECMAScript 5,因此与标准库存在一些重叠。然而,这是一个特性:在旧版浏览器上,您可以获得通常只有 ECMAScript-5 才有的功能;在 ECMAScript 5 上,相关函数简单地转发到标准库。
  • Lo-Dash是 Underscore.js API 的另一种实现,具有一些额外的功能。访问网站了解它是否比 Underscore.js 更适合您。
  • XRegExp是一个具有多个高级功能的正则表达式库,例如命名捕获和自由间隔(允许您将正则表达式分布在多行并逐行记录)。在幕后,增强的正则表达式被转换为普通的正则表达式,这意味着您在使用 XRegExp 时不会付出性能代价。

ECMAScript 国际化 API

ECMAScript 国际化 API 是一个标准的 JavaScript API,用于处理与国际化相关的任务:排序和搜索字符串、数字格式化以及日期和时间格式化。本节简要概述并指向更多阅读材料。

ECMAScript 国际化 API,第 1 版

API 的第一版提供了以下服务:

  • 排序支持两种场景:对一组字符串进行排序和在一组字符串中进行搜索。排序是由区域设置参数化的,并且了解 Unicode。
  • 数字格式化。参数包括:
  • 格式化样式:十进制、货币(由其他参数确定的货币种类和如何引用)
  • 区域设置(直接指定或最佳匹配,通过匹配器对象搜索)
  • 编号系统(西方数字、阿拉伯数字、泰国数字等)
  • 精度:整数位数、小数位数、有效数字位数
  • 分组分隔符打开或关闭
  • 日期和时间格式。参数包括:
  • 要格式化的信息以及使用哪种样式(短、长、数字等)
  • 一个区域设置
  • 一个时区

大部分功能通过全局变量Intl中的对象访问,但 API 还增强了以下方法:

  • String.prototype.localeCompare
  • Number.prototype.toLocaleString
  • Date.prototype.toLocaleString
  • Date.prototype.toLocaleDateString
  • Date.prototype.toLocaleTimeString
它是什么样的标准?

标准“ECMAScript 国际化 API”(EIA)的编号是 ECMA-402。它由 Ecma International 托管,该协会还托管 ECMA-262,即 ECMAScript 语言规范。这两个标准都由 TC39 维护。因此,EIA 是最接近语言的标准,而不是 ECMA-262 的一部分。该 API 已经设计用于与 ECMAScript 5 和 ECMAScript 6 一起使用。一套一致性测试补充了标准,并确保 API 的各种实现是兼容的(ECMA-262 有类似的测试套件)。

我什么时候可以使用它?

大多数现代浏览器已经支持它,或者正在支持它的过程中。David Storey 创建了一个详细的兼容性表(指示哪些浏览器支持哪些区域设置等)。

进一步阅读

ECMAScript 国际化 API 的规范由 Norbert Lindenberg 编辑。它以 PDF、HTML 和 EPUB 格式提供。此外,还有几篇全面的介绍性文章:

  • “ECMAScript 国际化 API” by Norbert Lindenberg
  • “ECMAScript 国际化 API” by David Storey
  • “使用 JavaScript 国际化 API” by Marcos Caceres

JavaScript 资源目录

本节描述了收集 JavaScript 资源信息的网站。有几种这样的目录。

以下是 JavaScript 的一般目录列表:

  • “JavaScriptOO:您应该关注的每个 JavaScript 项目”
  • JSDB:最好的 JavaScript 库集合
  • JSter:JavaScript 库和开发工具目录
  • “HTML5、JavaScript 和 CSS 资源的主列表”

专门的目录包括:

  • “Microjs:出色的微框架和微库,用于娱乐和利润”
  • “Unheap:整洁的 jQuery 插件存储库”

显然,您可以直接浏览包管理器的注册表:

  • npm(Node Packaged Modules)
  • Bower

CDN(内容交付网络)和 CDN 内容的目录包括:

  • jsDelivr:JavaScript 库、jQuery 插件、CSS 框架、字体等的免费 CDN
  • “cdnjs:JavaScript 和 CSS 的缺失 CDN”(托管不太流行的库)
致谢

以下人员为本节做出了贡献:Kyle Simpson(@getify),Gildas Lormeau(@check_ca),Fredrik Sogaard(@fredrik_sogaard),Gene Loparco(@gloparco),Manuel Strehl(@m_strehl)和 Elijah Manor(@elijahmanor)。

0 人点赞