第 26 章 元代码风格指南
目录
购买本书
(广告,请不要屏蔽。)

第 26 章 元代码风格指南

JavaScript 有很多很棒的代码风格指南。因此,没有必要再写一个。相反,本章描述了元代码风格规则,并调查了现有的代码风格指南和已建立的最佳实践。它还提到了我喜欢的、更具争议性的实践。其理念是补充现有的代码风格指南,而不是取代它们。

现有的代码风格指南

以下是我喜欢的代码风格指南

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

一般技巧

本节将介绍一些通用的代码编写技巧。

代码应该保持一致

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

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

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

代码应该易于理解

对于大多数代码来说,阅读代码的时间远远大于编写代码的时间。因此,尽可能简化阅读过程非常重要。以下是一些实现此目标的准则

短并不总是更好
有时,写得更多实际上意味着阅读速度更快。让我们考虑两个例子。首先,熟悉的东西更容易理解。这可能意味着使用熟悉的、稍微冗长一点的结构可能更好。其次,人类阅读的是标记,而不是字符。因此,redBalloonrdBlln 更容易阅读。
好的代码就像一本教科书

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

不要耍小聪明;不要让我思考
有很多聪明的代码利用对语言的深入了解来实现令人印象深刻的简洁性。这样的代码通常像一个谜题,难以理解。你会遇到这样的观点:如果人们不理解这样的代码,也许他们应该先真正学习 JavaScript。但这并不是我们讨论的重点。无论你多么聪明,进入别人的精神世界总是具有挑战性的。所以简单的代码并不愚蠢,它是那种大部分精力都花在让一切都易于理解上的代码。请注意,“其他人”包括你未来的自己。我经常发现我过去的一些聪明想法对现在的我来说毫无意义。
避免为了速度或代码大小而优化

很多聪明才智都集中在这些优化上。然而,你通常不需要它们。一方面,JavaScript 引擎正变得越来越智能,并自动优化遵循既定模式的代码的速度。另一方面,代码压缩工具(第 32 章)会重写你的代码,使其尽可能小。在这两种情况下,工具都为你提供了便利,因此你无需自己动手。

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

普遍接受的最佳实践

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

大括号风格

大括号分隔代码块的语言中,大括号风格决定了大括号的位置。在类 C 语言(如 Java 和 JavaScript)中,两种大括号风格最常见:Allman 风格和 1TBS。

1TBS(一种真正的括号风格)

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

// One True Brace Style
function foo(x, y, z) {
    if (x) {
        a();
    } else {
        b();
        c();
    }
}

1TBS 是(较旧的)K&R(Kernighan 和 Ritchie)风格的变体。[21] 在 K&R 风格中,函数以 Allman 风格编写,并且在不需要的地方省略大括号,例如,在单语句 then 语句周围

// K&R brace style
function foo(x, y, z)
{
    if (x)
        a();
    else {
        b();
        c();
    }
}

JavaScript

JavaScript 世界中的事实标准是 1TBS。它继承自 Java,大多数代码风格指南都推荐它。其中一个原因是客观的。如果你返回一个对象字面量,你必须将左大括号与关键字 return 放在同一行,如下所示(否则,自动分号插入会在 return 后面插入一个分号,这意味着什么也不返回;参见 陷阱:ASI 会意外地打断语句

return {
    name: 'Jane'
};

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

我个人的风格和偏好是

  • 1TBS(这意味着你尽可能使用大括号)。
  • 作为一个例外,如果一个语句可以写在一行中,我就会省略大括号。例如

    if (x) return x;

优先使用字面量而不是构造函数

一些构造函数生成的对象也可以由字面量创建。后者通常是更好的选择:

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创建具有给定元素的数组。使用元素初始化数组(避免!)解释了原因

var arr = new Array('a', 'b', 'c'); // never ever
var arr = [ 'a', 'b', 'c' ]; // yes

不要耍小聪明

本节收集了不推荐的耍小聪明的例子。

条件运算符

不要嵌套条件运算符:

// 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 语句

foo && bar(); // no
if (foo) bar(); // yes

foo || bar(); // no
if (!foo) bar(); // yes

递增运算符

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

// Unsure: what is happening?
return ++foo;

// Easy to understand
++foo;
return foo;

检查 undefined

if (x === void 0) x = 0; // not necessary in ES5
if (x === undefined) x = 0; // preferable

从 ECMAScript 5 开始,第二种检查方式更好。更改 undefined解释了原因。

将数字转换为整数

return x >> 0; // no
return Math.round(x); // yes

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

可以接受的聪明才智

有时你可以在 JavaScript 中耍小聪明——如果这种聪明才智已经成为一种既定的模式。

默认值

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

function f(x) {
    x = x || 0;
    ...
}

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

泛型方法

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

Object.prototype.hasOwnProperty.call(obj, propKey)
{}.hasOwnProperty.call(obj, propKey)

Array.prototype 可以缩写为 []

Array.prototype.slice.call(arguments)
[].slice.call(arguments)

我对这一点持矛盾的态度。这是一种 hack(你正在通过实例访问原型属性)。但它减少了混乱,我希望引擎最终能优化这种模式。

ECMAScript 5:尾随逗号

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

var obj = {
    first: 'Jane',
    last: 'Doe', // legal: trailing comma
};

ECMAScript 5:保留字

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

> var obj = { new: 'abc' };
> obj.new
'abc'

有争议的规则

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

语法

我们将从语法惯例开始

紧凑的空格

我喜欢相对紧凑的空格。该模型采用书面英语:左括号后和右括号前没有空格。逗号后有空格:

var result = foo('a', 'b');
var arr = [ 1, 2, 3 ];
if (flag) {
    ...
}

对于匿名函数,我遵循 Douglas Crockford 的规则,即关键字 function 后面有一个空格。理由是,如果您删除名称,这就是命名函数表达式的外观

function foo(arg) { ... }  // named function expression
function (arg) { ... }     // anonymous function expression
每个缩进级别四个空格
我看到的大多数代码都使用空格进行缩进,因为制表符在应用程序和操作系统之间的显示方式差异很大。我更喜欢每个缩进级别四个空格,因为这会使缩进更加明显。
将条件运算符放在括号中

这有助于阅读,因为更容易区分运算符的范围:

return result ? result : theDefault;  // no
return (result ? result : theDefault);  // yes

变量

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

每行一个变量声明

我不会使用单个声明声明多个变量:

// no
var foo = 3,
    bar = 2,
    baz;

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

这种方法的优点是删除、插入和重新排列行更简单,并且行会自动正确缩进。

保持变量声明局部化
如果您的函数不长(无论如何都不应该太长),那么您可以不用太在意提升,并假装 var 声明是块级作用域的。换句话说,您可以在使用变量的上下文中声明变量(在循环内、在 then 块或 else 块内等)。这种局部封装使代码片段更容易单独理解。删除代码片段或将其移动到其他位置也更容易。
如果您在块内,请留在该块内

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

// Don’t do this
if (v) {
    var x = v;
} else {
    var x = 10;
}
doSomethingWith(x);

前面的代码与以下代码具有相同的效果和意图,这就是为什么要这样编写代码的原因

var x;
if (v) {
    x = v;
} else {
    x = 10;
}
doSomethingWith(x);

面向对象

现在我们将介绍与面向对象相关的约定。

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

我建议您

  • 始终使用构造函数。
  • 创建实例时始终使用 new

这样做的主要优点是

  • 您的代码更适合 JavaScript 主流,并且更可能在框架之间移植。
  • 在现代引擎中,使用构造函数的实例速度非常快(例如,通过隐藏类)。
  • 类是即将推出的 ECMAScript 6 中的默认继承结构,它将基于构造函数。

对于构造函数,重要的是使用严格模式,因为它可以防止您忘记使用 new 运算符进行实例化。您应该知道您可以在构造函数中返回任何对象。有关使用构造函数的更多技巧,请参阅构造函数实现技巧

避免使用闭包来存储私有数据
如果您希望对象的私有数据完全安全,则必须使用闭包。否则,您可以使用普通属性。一种常见的做法是在私有属性的名称前加上下划线。闭包的问题在于代码变得更加复杂(除非您将所有方法都放在实例中,这是不符合习惯的并且速度很慢)并且速度更慢(访问闭包中的数据目前比访问属性慢)。保持数据私有更详细地介绍了此主题。
如果构造函数没有参数,则编写括号

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

var foo = new Foo;  // no
var foo = new Foo();  // yes
注意运算符优先级

使用括号,以便两个运算符不会相互竞争——结果并不总是您所期望的:

> false && true || true
true
> false && (true || true)
false
> (false && true) || true
true

instanceof 特别棘手

> ! {} instanceof Array
false
> (!{}) instanceof Array
false
> !({} instanceof Array)
true

但是,我发现构造函数后的方法调用没有问题

new Foo().bar().baz();  // ok
(new Foo()).bar().baz();  // not necessary

杂项

本节收集了各种技巧

强制转换

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

> +'123'  // no
123
> Number('123')  // yes
123

> ''+true  // no
'true'
> String(true)  // yes
'true'
避免将 this 作为隐式参数

this 应该仅指代当前方法调用的接收者;它不应该被滥用作隐式参数。理由是,此类函数更容易调用和理解。我还喜欢将面向对象和函数式机制分开:

// Avoid:
function handler() {
    this.logError(...);
}

// Prefer:
function handler(context) {
    context.logError(...);
}
通过 inhasOwnProperty 检查属性是否存在(请参阅属性的迭代和检测

这比与 undefined 进行比较或检查真值更具自我解释性且更安全

// 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 引擎和压缩器(请参阅第 32 章)。



[21] 有些人甚至说它们是同义词,1TBS 是一种开玩笑地指代 K&R 的方式。

下一页:27. 用于调试的语言机制