可维护JavaScript
第二章
注释
注释往往是编码过程中最不受关注的部分了.注释很接近于文档,但对开发者来说,编写文档已经是他们愿意花时间来做的最后一件事情了.然而,注释对于代码整体的可维护性来说,是非常重要的.尝试阅读一个没有任何注释的代码文件看起来像是一个有趣的挑战,但如果项目的截止时间非常紧迫,那么这样的任务会转变成折磨.适当的编写注释相当于讲述编写该代码的完整"故事",允许其他开发者不需要倾听故事的开始就能理解故事中的某一部分内容.风格指南并不会总能覆盖到注释的风格,但我认为,它已经足够重要来分出单独的一章来介绍了.
JavaScript支持两种不同类型的注释:单行注释和多行注释.
单行注释
单行注释以两个斜杠开始,行尾自动结束:
// 单行注释
Many prefer to include a space after the two slashes to offset the comment text. There are three ways in which a single-line comment is used:
- On its own line, explaining the line following the comment. The line should always be preceded by an empty line. The comment should be at the same indentation level as the following line.
- As a trailing comment at the end of a line of code. There should be at least one indent level between the code and the comment. The comment should not go beyond the maximum line length. If it does, then move the comment above the line of code.
- To comment out large portions of code (many editors automatically comment outmultiple lines).
Single-line comments should not be used on consecutive lines unless you’re commenting out large portions of code. Multiline comments should be used when long comment text is required.
Here are some examples:
// Good if (condition) { // if you made it here, then all security checks passed allowed(); } // Bad: No empty line preceding comment if (condition) { // if you made it here, then all security checks passed allowed(); } // Bad: Wrong indentation if (condition) { // if you made it here, then all security checks passed allowed(); } // Good var result = something + somethingElse; // somethingElse will never be null // Bad: Not enough space between code and comment var result = something + somethingElse;// somethingElse will never be null // Good // if (condition) { // doSomething(); // thenDoSomethingElse(); // } // Bad: This should be a multiline comment // This next piece of code is quite difficult, so let me explain. // What you want to do is determine whether the condition is true // and only then allow the user in. The condition is calculated // from several different functions and may change during the // lifetime of the session. if (condition) { // if you made it here, then all security checks passed allowed(); }
多行注释
多行注释以 /* 开头,以 */ 结尾,它可以跨越多个物理行,但也不是必须得这样,你也可以写在一行里.下面的这些都是合法的多行注释:
/* My comment */ /* Another comment. This one goes to two lines. */ /* Yet another comment. Also goes to a second line. */
虽然这些注释在语法上都是合法的,但我更喜欢Java风格的多行注释写法. The Java style is to have at least three lines: one for the /*, one or more lines beginning with a * that is aligned with the * on the previous line, and the last line for */. The resulting comment looks like this:
/* * Yet another comment. * Also goes to a second line. */
The result is a more legible comment that is visually aligned on the left to an asterisk.
IDEs such as NetBeans and Eclipse will automatically insert these leading asterisks for
you.
Multiline comments always come immediately before the code that they describe. As
with single-line comments, multiline comments should be preceded by an empty line
and should be at the same indentation level as the code being described. Here are some
examples:
// Good if (condition) { /* * if you made it here, * then all security checks passed */ allowed(); } // Bad: No empty line preceding comment if (condition) { /* * if you made it here, * then all security checks passed */ allowed(); } // Bad: Missing a space after asterisk if (condition) { /* *if you made it here, *then all security checks passed */ allowed(); } // Bad: Wrong indentation if (condition) { /* * if you made it here, * then all security checks passed */ allowed(); } // Bad: Don't use multiline comments for trailing comments var result = something + somethingElse; /*somethingElse will never be null*/
Using Comments
When to comment is a topic that always fosters great debate among developers. The
general guidance is to comment when something is unclear and not to comment when
something is apparent from the code itself. For example, the comment in this example
doesn’t add any understanding to the code:
// Bad // Initialize count var count = 10;
It’s apparent from just the code that count is being initialized. The comment adds no
value whatsoever. If, on the other hand, the value 10 has some special meaning that
you couldn’t possibly know from looking at the code, then a comment would be very
useful:
// Good // Changing this value will make it rain frogs var count = 10;
As implausible as it may be to make it rain frogs by changing the value of count, this is
an example of a good comment, because it tells you something that you otherwise
would be unaware of. Imagine how confused you would be if you changed the value
and it started to rain frogs…all because a comment was missing.
So the general rule is to add comments where they clarify the code.
Difficult-to-Understand Code
Difficult-to-understand code should always be commented. Depending on what the
code is doing, you may use one multiline comment, several single comments, or some
combination thereof. They key is to bring some understanding of the code’s purpose
to someone else. For example, here’s some code from the YUI library’s Y.mix() method:
// Good if (mode) { /* * In mode 2 (prototype to prototype and object to object), we recurse * once to do the proto to proto mix. The object to object mix will be * handled later on. */ if (mode === 2) { Y.mix(receiver.prototype, supplier.prototype, overwrite, whitelist, 0, merge); } /* * Depending on which mode is specified, we may be copying from or to * the prototypes of the supplier and receiver. */ from = mode === 1 || mode === 3 ? supplier.prototype : supplier; to = mode === 1 || mode === 4 ? receiver.prototype : receiver; /* * If either the supplier or receiver doesn't actually have a * prototype property, then we could end up with an undefined from * or to. If that happens, we abort and return the receiver. */ if (!from || !to) { return receiver; } } else { from = supplier; to = receiver; }
The Y.mix() method uses constants to determine how to proceed. The mode argument
is equivalent to one of those constants, but it’s hard to understand what each constant
means just from the numeric value. The code is commented well, because it explains
what otherwise appear to be complex decisions.
Potential Author Errors
Another good time to comment code is when the code appears to have an error. Teams often get bitten by well-meaning developers who find some code that looks problematic, so they fix it. Except that the code wasn’t the source of a problem, so “fixing” it actually creates a problem that needs to be tracked down. Whenever you’re writingcode that could appear incorrect to another developer, make sure to include a comment.Here’s another example from YUI:
while (element &&(element = element[axis])) { // NOTE: assignment if ( (all || element[TAG_NAME]) && (!fn || fn(element)) ) { return element; } }
In this case, the developer used an assignment operator in the while loop control con-
dition. This isn’t standard practice and will typically be flagged by linting tools as a
problem. If you were unfamiliar with this code and came across this line without a
comment, it would be easy to assume that this was an error, and the author meant to
use the equality operator == instead of the assignment operator =. The trailing comment
on that line indicates the use of the assignment operator is intentional. Now any other
developer who comes along and reads the code won’t be likely to make a bad “fix.”
Browser-Specific Hacks
JavaScript developers are often forced to use code that is inefficient, inelegant, or
downright dirty to get older browsers to work correctly. This behavior is actually a
special type of potential author error: code that isn’t obviously doing something
browser-specific may appear to be an error. Here’s an example from the YUI library’s
Y.DOM.contains() method:
var ret = false; if ( !needle || !element || !needle[NODE_TYPE] || !element[NODE_TYPE]) { ret = false; } else if (element[CONTAINS]) { // IE & SAF contains fail if needle not an ELEMENT_NODE if (Y.UA.opera || needle[NODE_TYPE] === 1) { ret = element[CONTAINS](needle); } else { ret = Y_DOM._bruteContains(element, needle); } } else if (element[COMPARE_DOCUMENT_POSITION]) { // gecko if (element === needle || !!(element[COMPARE_DOCUMENT_POSITION](needle) & 16)) { ret = true; } } return ret;
Line 6 of this code has a very important comment. Even though Internet Explorer and
Safari both include the contains() method natively, the method will fail if needle is not
an element. So the method should be used only if the browser is Opera or needle is an
element (nodeType is 1). The note about the browsers, and also why the if statement is
needed, not only ensures that no one will change it unexpectedly in the future, butallows the author to revisit this code later and realize that it may be time to verify whether newer versions of Internet Explorer and Safari show the same issue.
Documentation Comments
Documentation comments aren’t technically part of JavaScript, but they are a very
common practice. Document comments may take many forms, but the most popular
is the form that matches JavaDoc documentation format: a multiline comment with an
extra asterisk at the beginning (/**) followed by a description, followed by one or more
attributes indicated by the @ sign. Here’s an example from YUI:
/** Returns a new object containing all of the properties of all the supplied objects. The properties from later objects will overwrite those in earlier objects. Passing in a single object will create a shallow copy of it. For a deep copy, use `clone()`. @method merge @param {Object} objects* One or more objects to merge. @return {Object} A new merged object. **/ Y.merge = function () { var args = arguments, i = 0, len = args.length, result = {}; for (; i < len; ++i) { Y.mix(result, args[i], true); } return result; };
The YUI library uses its own tool called YUIDoc to generate documentation from these
comments. However, the format is almost exactly the same as the library-agnostic
JSDoc Toolkit, which is widely used on open source projects as well as within Google.
The key difference between YUIDoc and JSDoc Toolkit is that YUIDoc supports both
HTML and Markdown in documentation comments, whereas JSDoc Toolkit supports
only HTML.
It is highly recommended that you use a documentation generator with your JavaScript.
The format of the comments must match the tool that you use, but the JavaDoc-style
documentation comments are well supported across many documentation generators.
When using documentation comments, you should be sure to document the following:
- All methods
Be sure to include a description of the method, expected arguments, and possible return values.
- All constructors
Comments should include the purpose of the custom type and expected arguments.
- All objects with documented methods
If an object has one or more methods with documentation comments, then it also must be documented for proper documentation generation.
Of course, the exact comment format and how comments should be used will ultimately be determined by the documentation generator you choose.
第三章
语句和表达式
在JavaScript中,一些语句比如if语句和for语句有两种使用形式,一种是使用大括号来包含多条语句,另一种是不用大括号但只能包含一条语句.例如:
// Bad,虽说在JavaScript中这样写也是合法的 if(condition)
doSomething();
// Bad,虽说在JavaScript中这样写也是合法的 if(condition) doSomething();
// Good if (condition) {
doSomething();
}
// Bad,虽说在JavaScript中这样写也是合法的 if (condition) { doSomething(); }
前两条语句中,if语句没有加大括号,这种形式是被Crockford的代码规范,jQuery核心风格指南,SproutCore风格指南,以及Dojo风格指南明确禁止的.JSLint和JSHint也会对这种缺失大括号的语句发出警告.
绝大多数的JavaScript开发人员都一致认为:块语句应该始终使用大括号,且始终要占据多行,而是单独一行.这是因为,如果你省略了大括号,可能会让别人产生困惑.考虑下面的代码:
if (condition)
doSomething();
doSomethingElse();
看了这样的代码,很难说作者的意图到底是什么.这里肯定是有错误的,但我们无法知道这个错误是缩进错误(最后一行不应该缩进)还是大括号忘写了(第二行和第三行应该放在一个语句块中).如果这里有大括号,我们就可以很容易的知道错误所在.下面演示另外两个错误:
if (condition) {
doSomething();
}
doSomethingElse();
if (condition) {
doSomething();
doSomethingElse();
}
这两个例子中,代码的错误很明显,全部是缩进错误.通过大括号你可以很快的推断出作者的意图,从而修正这个错误,完全不需要担心可能会改变原代码的逻辑.
在下面的这几个块语句中,都应该使用大括号:
- if
- for
- while
- do...while
- try...catch...finally
大括号的对齐方式
关于块语句的下一个话题就是大括号的对齐方式.大括号的对齐风格主要有两种.第一种风格是把左大括号放在语句块开始的第一行,如下例:
if (condition) {
doSomething();
} else {
doSomethingElse();
}
JavaScript从Java中继承了这种风格,这种写法是被记录在Java编程语言的代码规范中的.这种风格还出现Crockford的代码规范,jQuery核心风格指南,SproutCore风格指南,Google JavaScript风格指南,以及Dojo风格指南中.
大括号对齐的第二种风格是把左大括号放在块语句开始的下一行,并且独占一行,如下例:
if (condition)
{
doSomething();
}
else { doSomethingElse(); }
这种风格是从C#中流行起来的,因为Visual Studio会强制进行这种对齐方式.但并没有主流的JavaScript指南推荐这种风格,而且Google JavaScript 风格指南还明确禁止了这种写法,理由是为了防止因为自动分号插入引发的错误.我的建议也是使用第一种大括号对齐方式.
块语句中的间隔
块语句第一行中的各部分之间要不要加间隔,这也是一个见仁见智的问题.关于这个问题,一共有三种主要的风格.第一种是,在语句名称,左小括号以及左大括号之间不要任何的空格:
if(condition){
doSomething();
}
一些程序员喜欢这种风格,是因为这样写更紧凑,但另外一些人会说,紧凑同样意味着可读性降低.Dojo风格指南推荐这种风格.
第二种风格是在左小括号和右小括号外侧的两边各加一个空格,如下:
if (condition) {
doSomething();
}
一些程序员喜欢这种风格,是因为这样写能让语句类型和条件表达式更加清晰易读.Crockford的代码规范和Google JavaScript风格指南推荐这种风格.
第三种风格是在第二种风格的基础上,在左小括号和右小括号内侧的两边再各加一个空格,如下:
if ( condition ) {
doSomething();
}
jQuery核心风格指南推荐这种写法,因为它能让语句各个部分都清晰可辨.
我喜欢第二种风格,这也是第一种和第三种风格的折衷.
switch语句
Developers tend to have a love-hate relationship with the switch statement. There are
varying ideas about how to use switch statements and how to format them. Some of
this variance comes from the switch statement’s lineage, originating in C and making
its way through Java into JavaScript without the exact same syntax.
Despite the similar syntax, JavaScript switch statements behave differently than in other
languages: any type of value may be used in a switch statement, and any expression
can be used as a valid case. Other languages require the use of primitive values and
constants, respectively.
缩进
Indentation of the switch statement is a matter of debate among JavaScript developers.
Many use the Java style of formatting switch statements, which looks like this:
switch(condition) {
case "first":
// code break;
case "second":
// code break;
case "third":
// code break;
default:
// code }
The unique parts of this format are:
- Each case statement is indented one level from the switch keyword.
- There is an extra line before and after each case statement from the second one on.
The format of switch statements is rarely included in style guides when this style is
used, primarily because it is the format that many editors use automatically.
Although this is the format that I prefer, both Crockford’s Code Conventions and the
Dojo Style Guide recommend a slightly different format:
switch(condition) {
case "first":
// code break;
case "second":
// code break;
case "third":
// code break;
default:
// code }
The major difference between this and the previous format is that the case keyword is
aligned to the same column as the switch keyword. Note also that there are no blank
lines in between any parts of the statement. JSLint expects this indentation format for
switch statements by default and will warn if a case is not aligned with switch. This
option may also be turned on and off via the “Tolerate messy white space” option.
JSLint does not warn if additional blank lines are included.
As with other aspects of coding style, this choice is completely a matter of preference.
Falling Through
Another popular source of debate is whether falling through from one case to another
is an acceptable practice. Accidentally omitting a break at the end of a case is a very
common source of bugs, so Douglas Crockford argues that every case should end with
break, return, or throw, without exception. JSLint warns when one case falls through
into another.
I agree with those who consider falling through to be an acceptable method of pro-
gramming, as long as it is clearly indicated, such as:
switch(condition) {
// obvious fall through case "first":
case "second":
// code break;
case "third":
// code /*falls through*/
default:
// code }
This switch statement has two obvious fall-throughs. The first case falls through into
the second, which is considered an acceptable practice (even by JSLint) because there
are no statements to run for just the first case and there are no extra lines separating
the two case statements.
The second instance is with case "third", which falls through into the default handler.
This fall-through is marked with a comment to indicate developer intent. In this code,
it’s obvious that the case is meant to fall through and isn’t a mistake. JSHint typically
warns when a case falls through unless you include this comment, in which case the
warning is turned off because you’ve signaled that this isn’t an error.
Crockford’s Code Conventions disallows fall-throughs in switch statements altogether.
The jQuery Core Style Guide mentions that fall-throughs are used in their code, and
the Dojo Style Guide gives an example with a fall-through comment. My recommen-
dation is to allow fall-throughs as long as a comment is used to indicate that the fall-
through is intentional.
default
Another point of contention with regard to switch is whether a default case is required.
Some believe that a default should always be included even if the default action is to
do nothing, as in:
switch(condition) {
case "first":
// code break;
case "second":
// code break;
default:
// do nothing }
You’re likely to find open source JavaScript code following this pattern, including
default and just leaving a comment that nothing should happen there. Although no
style guides are explicit about this, both Douglas Crockford’s Code Conventions for
the JavaScript Programming Language and the Dojo Style Guide include default as
part of their standard switch statement format.
My preference is to omit default when there is no default action and annotate it using
a comment, as in this example:
switch(condition) {
case "first":
// code break;
case "second":
// code break;
// no default }
This way, the code author’s intent is clear that there should be no default action, and
you save some bytes by not including extra unnecessary syntax.
with语句
The with statement changes how the containing context interprets variables. It allows
properties and methods from a particular object to be accessed as if they were local
variables and functions, omitting the object identifier altogether. The intent of with was
to lessen the amount of typing developers need to do when using multiple object mem-
bers in close proximity. For example:
var book = {
title: "Maintainable JavaScript",
author: "Nicholas C. Zakas" }; var message = "The book is ";
with (book) {
message += title;
message += " by " + author;
}
In this code, the with statement is used to augment identifier resolution within the curly
braces by allowing the properties of book to be accessed as if they were variables. The
problem is that it’s hard to tell where title and author originated from. It’s not clear
that these are properties of book and that message is a local variable. This confusion
actually extends far beyond developers, with JavaScript engines and minifiers being
forced to skip optimization of this section for fear of guessing incorrectly.
The with statement is actually disallowed in strict mode, causing a syntax error and
indicating the ECMAScript committee’s belief that with should no longer be used.
Crockford’s Code Conventions and the Google JavaScript Style Guide disallow the use
of with. I strongly recommend avoiding the with statement, as it prevents you from
easily applying strict mode to your code (a practice I recommend).
for循环
There are two types of for loops: the traditional for loop that JavaScript inherited from
C and Java, as well as the for-in loop that iterates over properties for an object. These
two loops, though similar, have two very different uses. The traditional for loop is
typically used to iterate over members of an array, such as:
var values = [ 1, 2, 3, 4, 5, 6, 7 ],
i, len;
for (i=0, len=values.length; i < len; i++) {
process(values[i]);
}
There are two ways to modify how the loop proceeds (aside from using a return or
throw statement). The first is to use the break statement. Using break causes the loop to exit immediately and not continue running even if the loop hasn’t finished all iter-
ations. For example:
var values = [ 1, 2, 3, 4, 5, 6, 7 ],
i, len;
for (i=0, len=values.length; i < len; i++) {
if (i == 2) {
break; // no more iterations }
process(values[i]);
}
The body of this loop will execute two times and then exit before executing
process() the third time, even if the values array has more than three items.
The second way to modify how a loop proceeds is through the use of continue. The
continue statement exits the loop immediately; however, the loop will continue with
the next iteration. Here’s an example:
var values = [ 1, 2, 3, 4, 5, 6, 7 ],
i, len;
for (i=0, len=values.length; i < len; i++) {
if (i == 2) {
continue; // skip just this iteration }
process(values[i]);
}
The body of this loop executes two times, skips the third time, and picks up with the
fourth iteration. The loop will then continue until its last iteration unless otherwise
interfered with.
Crockford’s Code Conventions disallows the use of continue. His assertion is that code
using continue can better be written using conditions. For instance, the previous ex-
ample can be rewritten as:
var values = [ 1, 2, 3, 4, 5, 6, 7 ],
i, len;
for (i=0, len=values.length; i < len; i++) {
if (i != 2) {
process(values[i]);
}
}
Crockford argues that this pattern is easier for developers to understand and less error
prone. The Dojo Style Guide states explicitly that continue, along with break, may be
used. My recommendation is to avoid continue whenever possible, but there is no
reason to completely forbid it. The readability of the code should dictate its usage.
JSLint warns when continue is used. JSHint does not warn when continue is used.
for-in循环
The for-in loop is used to iterate over properties of an object. Instead of defining a
control condition, the loop systematically goes through each named object property
and returns the property name inside of a variable, as in:
var prop;
for (prop in object) {
console.log("Property name is " + prop);
console.log("Property value is " + object[prop]);
}
A problem with for-in is that it returns not only instance properties of an object
but also all properties it inherits through the prototype. You may thus end up with
unanticipated results when iterating through properties on your own object. For this
reason, it’s best to filter the for-in loop to only instance properties by using
hasOwnProperty(). Here’s an example:
var prop;
for (prop in object) {
if (object.hasOwnProperty(prop)) {
console.log("Property name is " + prop);
console.log("Property value is " + object[prop]);
}
}
Crockford’s Code Conventions require the use of hasOwnProperty() for all for-in loops.
Both JSLint and JSHint warn when a for-in loop is missing a call to
hasOwnProperty() by default (both allow this option to be turned off). My recommen-
dation is to always use hasOwnProperty() for for-in loops unless you’re intentionally
looking up the prototype chain, in which case it should be indicated with a comment,
such as:
var prop;
for (prop in object) { // include prototype properties console.log("Property name is " + prop);
console.log("Property value is " + object[prop]);
}
Another area of focus with for-in loops is their usage with objects. A common mistake
is to use for-in to iterate over members of an array, as in this example:
// Bad var values = [ 1, 2, 3, 4, 5, 6, 7],
i;
for (i in values) {
process(items[i]);
}
This practice is disallowed in Crockford’s Code Conventions as well as the Google
JavaScript Style Guide due to the potential errors it may cause. Remember, the for-
in is iterating over object keys on both the instance and the prototype, so it’s not limited
to the numerically indexed properties of the array. The for-in loop should never be
used in this way.
第四章
变量,函数和操作符
任何JavaScript程序的真正核心部分是那些自己写的用来完成特定任务的众多函数.在函数内部,变量和操作符是用来进行运算和其他一些操作的. 这就是为什么要在你的JavaScript代码基本格式弄好后,就来决定一下怎样使用函数,变量和操作符才能减少代码的复杂性并且提高可读性,这是很重要的.
变量声明
变量声明是使用var语句来完成的.JavaScript允许在一个脚本内的任意地方使用var语句任意多次.这种用法让开发者有了一种有趣的认知问题,因为所有的var语句都会被提升到包含该语句的函数的顶部而不论它们的实际位置在哪儿.例如:
function doSomething() {
var result = 10 + value;
var value = 10;
return result;
}
在上面的这段代码中,在变量value声明之前就使用它是完全合法的,尽管这样做会导致result的值为NaN.想要明白为什么可以这样使用,你需要知道你的这段代码实际上会被JavaScript引擎处理成这样:
function doSomething() {
var result;
var value;
result = 10 + value;
value = 10;
return result;
}
两个var语句都被提升到函数的顶部,而初始化是在这之后进行的.为初始化之前,变量的值为undefined,正如在第6行一样,导致result的值为NaN (not a number).然后value的值才会被赋值为10.
有一个地方的var语句会让开发者们有点忘记了这里的变量声明也是会被提升的,这个地方就是for语句中,如下面的代码中,变量声明处在for语句的初始化部分:
function doSomethingWithItems(items) {
for (var i=0, len=items.length; i < len; i++) {
doSomething(items[i]);
}
}
JavaScript一直到ECMAScript 5仍然没有块级作用域内的变量声明的概念, 所以上面的代码实际上等价于:
function doSomethingWithItems(items) {
var i, len;
for (i=0, len=items.length; i < len; i++) {
doSomething(items[i]);
}
}
变量声明的提升意味着在一个函数内部的任意位置定义的变量都相当于在函数的顶部定义一样.因此,一种很流行的风格就是把所有的变量声明都放在函数顶部而不是分散在整个函数里.也就是说,你写的代码刚好符合了JavaScript引擎理解它的习惯.
我的建议是将你的所有局部变量都定义在函数内的第一个语句中.这种方法也被推荐在Crockford的编码规范,SproutCore风格指南,以及Dojo风格指南中:
function doSomethingWithItems(items) {
var i, len;
var value = 10;
var result = value + 10;
for (i=0, len=items.length; i < len; i++) {
doSomething(items[i]);
}
}
Crockford继续推荐了在一个函数的顶部使用一个单一的var语句的方式:
function doSomethingWithItems(items) {
var i, len,
value = 10,
result = value + 10;
for (i=0, len=items.length; i < len; i++) {
doSomething(items[i]);
}
}
Dojo风格指南仅允许在一个变量与另一个变量具有一定相关性时,才将这两个var语句合并为一个.
我的个人喜好是将所有的var语句都合并,并且每个变量声明都放在单独的一行内.等号也要对齐.那些没有初始化的变量,应该放在语句后面的部分,如下:
function doSomethingWithItems(items) {
var value = 10,
result = value + 10,
i,
len;
for (i=0, len=items.length; i < len; i++) {
doSomething(items[i]);
}
}
我建议,最起码,也将所有的var语句合并,这样做可以减小你的代码大小,因此加快代码文件的下载.
函数声明
函数声明,和变量声明一样,也是会被JavaScript引擎提升的.因此,在一个函数声明的位置之前使用该函数也是可以的:
// Bad
doSomething();
function doSomething() {
alert("Hello world!");
}
这样可以正常运行是因为JavaScript引擎会将代码理解成下面这样:
// Bad
function doSomething() {
alert("Hello world!");
}
doSomething();
正因如此,所以在JavaScript里,推荐函数始终在使用之前首先声明.这条推荐也出现在Crockford的编码规范中.Crockford也推荐局部函数应该放在包含它的函数中紧跟着变量声明之后的位置,如下:
function doSomethingWithItems(items) {
var i, len,
value = 10,
result = value + 10;
function doSomething(item) {
// 其他代码
}
for (i=0, len=items.length; i < len; i++) {
doSomething(items[i]);
}
}
如果一个函数在他声明之前就进行调用,则JSLint 和 JSHint会发出警告.
另外,函数声明不应该出现在任何的块级语句内部.例如,下面的代码不会按照所预想的那样运行:
// Bad
if (condition) {
function doSomething() {
alert("Hi!");
}
} else {
function doSomething() {
alert("Yo!");
}
}
究竟这段代码会如何运行,这会因浏览器的不同而不同.大多数的浏览器会自动的选取第二个函数声明,不管condition的值是真是假,Firefox会根据condition的值使用对应条件分支内部的function声明.这是ECMAScript规范的一个灰色地带,因此,应该避免写这样的代码.函数声明应该始终写在条件语句外部.这种模式在谷歌的JavaScript风格指南中被明确禁止.
Function Call Spacing
Almost universally, the recommended style for function calls is to have no space be-
tween the function name and the opening parenthesis, which is done to differentiate it
from a block statement. For example:
// Good
doSomething(item);
// Bad: Looks like a block statement
doSomething (item);
// Block statement for comparison
while (item) {
// do something
}
Crockford’s Code Conventions explicitly calls this out. The Dojo Style Guide, Sprout-
Core Style Guide, and Google JavaScript Style Guide implicitly recommend this style
through code examples.
The jQuery Core Style Guide further specifies that an extra space should be included
after the opening parenthesis and before the closing parenthesis, such as:
// jQuery-style
doSomething( item );
The intent here is to make the arguments easier to read. The jQuery Core Style
Guide also lists some exceptions to this style, specifically relating to functions that are
passed a single argument that is an object literal, array literal, function expression, or
string. So the following examples are all still considered valid:
// jQuery exceptions
doSomething(function() {});
doSomething({ item: item });
doSomething([ item ]);
doSomething("Hi!");
Generally speaking, styles with more than one exception are not good, because they
can be confusing to developers.
第六章
避免全局变量
JavaScript的执行环境在很多方面是独一无二的.其中一个就是在全局变量(也包括全局函数)的使用上.默认的JavaScript执行环境,实际上是在脚本开始执行的时候由许许多多的全局变量组成的.所有的这些变量都是被定义在一个称为全局对象的对象上的,这是个神秘的对象表示了当前脚本在执行时最外层的上下文.
在浏览器中,window对象通常也被重载为全局对象,所以在全局作用域内定义的变量和函数都会成为window对象的属性.例如:
var color = "red"
function sayColor() {
alert(color);
}
console.log(window.color); // "red"
console.log(typeof window.sayColor); // "function"
上面的代码中,全局变量color和全局函数sayColor()定义后,全部被作为属性添加到了window对象上,虽然并没有明确指定这样做.
全局变量引发的问题
创建全局变量通常被认为是一个不好的习惯,如果在团队开发环境中,是有可能引发问题的. Globals create a number of nontrivial maintenance issues for code going forward. The more globals, the greater the possibility that errors will be introduced due to the increasing likelihood of a few common problems.
命名冲突
The potential for naming collisions increases as the number of global variables and functions increase in a script, as do the chances that you’ll use an already declared variable accidentally. The easiest code to maintain is code in which all of its variables are defined locally.
For instance, consider the sayColor() function from the previous example. This func-tion relies on the global variable color to function correctly. If sayColor() were defined in a separate file than color, it would be hard to track down:
function sayColor() {
alert(color); // Bad: where'd this come from?
}
Further, if color ends up being defined in more than one place, the result of say Color() could be different depending on how this function is included with the other code.
The global environment is where native JavaScript objects are defined, and by adding your own names into that scope, you run the risk of picking a name that might be provided natively by the browser later on. The name color, for example, is definitely not a safe global variable name. It’s a plain noun without any qualifiers, so the chances of collision with an upcoming native API, or another developer’s work, is quite high.
Code Fragility
A function that depends on globals is tightly coupled to the environment. If the environment changes, the function is likely to break. In the previous example, the say Color() method will throw an error if the global color variable no longer exists. That means any change to the global environment is capable of causing errors throughout the code. Also, globals can be changed at any point by any function, making the reliability of their values highly suspect. The function from the previous example is much more maintainable if color is passed in as an argument:
function sayColor(color) {
alert(color);
}
This version of the function has no global dependencies and thus won’t be affected by changes to the global environment. Because color is now a named argument, all that matters is that a valid value is passed into the function. Other changes will not affect this function’s ability to complete its task.
When defining functions, it’s best to have as much data as possible local to the function.Anything that can be defined within the function should be written as such; any data that comes from outside the function should be passed in as an argument. Doing so isolates the function from the surrounding environment and allows you to make changes to either without affecting the other.
第七章
事件处理
事件处理是所有JavaScript应用程序的重要组成部分.所有的JavaScript代码都是通过事件与UI界面联系在一起的,所以实际上大部分web开发者编程的时间都是在编写事件处理器中的代码.不幸的是,事件处理也是JavaScript编程中长期以来没有受到足够关注的一个方面.虽然JavaScript开发者们开始拥抱更加传统的架构概念,事件处理仍是属于那些鲜有变化的领域.大多数事件处理代码都与事件环境紧密耦合的(在事件触发时会提供给开发者什么) ,因此不是很容易维护.
经典用法
大部分开发者应该都很熟悉在事件触发时传入响应事件处理函数中的事件对象.这个事件对象包含了所有与本次事件相关的信息,包括事件目标以及其他一些因事件类型不同而不同的额外数据,鼠标事件会在事件对象上暴露额外的位置信息,键盘事件会暴露出具体是那个键盘按键触发了本次事件,以及触摸事件会暴露出触摸的位置和持续时间等信息.正因为有这些信息,UI界面才能作出适当的反应.
然而,在许多情况下,你所使用的信息仅仅是事件对象上提供的信息的一个很小的子集.考虑下面的代码:
// Bad
function handleClick(event) {
var popup = document.getElementById("popup");
popup.style.left = event.clientX + "px";
popup.style.top = event.clientY + "px";
popup.className = "reveal";
}
// addListener() from Chapter 7
addListener(element, "click", handleClick);
这段代码中仅仅使用到了所传入事件对象上的两个属性:clientX和clientY.这两个属性用来在向用户显示页面上的某个元素之前重新定位这个元素.虽然这段代码看起来比较简单,貌似没有问题,但实际上这是一种不好的代码模式,因为它会引入一些限制.
Rule #1: Separate Application Logic
The previous example’s first problem is that the event handler contains application
logic. Application logic is functionality that is related to the application rather than
related to the user’s action. In the previous example, the application logic is displaying
a pop up in a particular location. Even though this action should happen when the user
clicks on a particular element, this may not always be the case.
It’s always best to split application logic from any event handler, because the same logic
may need to be triggered by different actions in the future. For example, you may decide
later that the pop up should be displayed when the user moves the cursor over the
element, or when a particular key is pressed on the keyboard. Then you may end up
accidentally duplicating the code into a second or third event handler attaching the
same event handler to handle multiple events.
Another downside to keeping application logic in the event handler is for testing. Tests
need to trigger functionality directly without going through the overhead of actually
having someone click an element to get a reaction. By having application logic inside
of event handlers, the only way to test is by causing the event to fire. That’s usually not
the best way to test, even though some testing frameworks are capable of simulating
events. It would be better to trigger the functionality with a simple function call.
You should always separate application logic from event-handling code. The first step
in refactoring the previous example is to move the pop up–handling code into its own
function, which will likely be on the one global object you’ve defined for your appli-
cation. The event handler should also be on the same global object, so you end up with
two methods:
// Better - separate application logic
var MyApplication = {
handleClick: function(event) {
this.showPopup(event);
},
showPopup: function(event) {
var popup = document.getElementById("popup");
popup.style.left = event.clientX + "px";
popup.style.top = event.clientY + "px";
popup.className = "reveal";
}
};
addListener(element, "click", function(event) {
MyApplication.handleClick(event);
});
The MyApplication.showPopup() method now contains all of the application logic pre-
viously contained in the event handler. The MyApplication.handleClick() method now
does nothing but call MyApplication.showPopup(). With the application logic separated
out, it’s easier to trigger the same functionality from multiple points within the appli-
cation without relying on specific events to fire. But this is just the first step in unraveling
this event-handling code.
Rule #2: Don’t Pass the Event Object Around
After splitting out application logic, the next problem with the previous example is that
the event object is passed around. It’s passed from the anonymous event handler to
MyApplication.handleClick(), then to MyApplication.showPopup(). As mentioned pre-
viously, the event object has potentially dozens of additional pieces of information
about the event, and this code only uses two of them.
Application logic should never rely on the event object to function properly for the
following reasons:
- The method interface makes it unclear what pieces of data are actually necessary.Good APIs are transparent in their expectations and dependencies; passing the event object as an argument doesn’t give you any idea what it’s doing with which pieces of data.
- Because of that, you need to recreate an event object in order to test the method.Therefore, you’ll need to know exactly what the method is using to write a proper stub for testing.
These issues are both undesirable in a large-scale web application. Lack of clarity is
what leads to bugs.
The best approach is to let the event handler use the event object to handle the event
and then hand off any required data to the application logic. For example, the MyAppli
cation.showPopup() method requires only two pieces of data: an x-coordinate and a y-
coordinate. The method should then be rewritten to accept those as arguments:
// Good
var MyApplication = {
handleClick: function(event) {
this.showPopup(event.clientX, event.clientY);
},
showPopup: function(x, y) {
var popup = document.getElementById("popup");
popup.style.left = x + "px";
popup.style.top = y + "px";
popup.className = "reveal";
}
};
addListener(element, "click", function(event) {
MyApplication.handleClick(event); // this is okay
});
In this rewritten code, MyApplication.handleClick() now passes in the x-coordinate
and y-coordinate to MyApplication.showPopup() instead of passing the entire event ob-
ject. It’s very clear what MyApplication.showPopup() expects to be passed in, and it’s
quite easy to call that logic directly from a test or elsewhere in the code, such as:
// Great victory!
MyApplication.showPopup(10, 10);
When handling events, it is best to let the event handler be the only function that
touches the event object. The event handler should do everything necessary using the
event object before delegating to some application logic. Thus actions such as pre-
venting the default action or stopping event bubbling should be done strictly in the
event handler, as in:
// Good
var MyApplication = {
handleClick: function(event) {
// assume DOM Level 2 events support
event.preventDefault();
event.stopPropagation();
// pass to application logic
this.showPopup(event.clientX, event.clientY);
},
showPopup: function(x, y) {
var popup = document.getElementById("popup");
popup.style.left = x + "px";
popup.style.top = y + "px";
popup.className = "reveal";
}
};
addListener(element, "click", function(event) {
MyApplication.handleClick(event); // this is okay
});
In this code, MyApplication.handleClick() is the defined event handler, so it makes the
calls to event.preventDefault() and event.stopPropagation() before passing data to
the application logic, which is exactly how the relationship between event handlers and
the application should work. Because the application logic no longer depends on
event, it’s easy to use that same logic in multiple places as well as to write tests.
第八章
避免空比较
在JavaScript中,一种常见的但却有问题的操作就是用一个变量的值和null做比较,可能是想通过这种方法来判断出一个变量中是否包含一个合适的值,例如:
var Controller = {
process: function(items) {
if (items !== null) { // Bad
items.sort();
items.forEach(function(item) {
// 其他代码
});
}
}
};
在这里,很明显process()方法期望变量items为一个数组类型的值,这个可以从所使用的sort()和forEach()方法看出来.代码中应该有一个条件判断语句来进行这样的判断:不要继续处理数据除非参数items为一个数组变量.但问题是,实际上该方法中的条件判断是和null做比较的,这样并不能阻止可能发生的错误.因为变量items的值还可以为1,或者一个字符串,或者其他对象.所有这些值都是不和null相等的,却会引发process()方法在执行sort()方法时就会报错.
仅仅通过将一个变量的值与null做比较基本上不会得出足够的信息来确定操作是否可以继续进行.幸运的是,JavaScript给了你很多方法来确定一个变量的值的具体类型.
检测原始值
在JavaScript中一共有五种类型的原始值:字符串, 数字, 布尔值, null, undefined.如果你期望一个值的类型是上述五种类型中除null之外的任一种,则typeof操作符是你最好的选择.typeof操作符会返回一个字符串代表某个值的类型:
• 对于字符串, typeof 返回 “string.”
• 对于数字, typeof 返回 “number.”
• 对于布尔值, typeof 返回 “boolean.”
• 对于undefined, typeof 返回 “undefined.”
typeof的基本语法如下:
typeof variable
你也可以加上括号
typeof(variable)
虽然这也是JavaScript的正确语法,但这样写会让typeof看起来像个函数而不是操作符.因此,还是推荐不适用括号的方式.
使用typeof来检测这四种原始值的类型是最安全可靠的办法.下面是一些例子:
// 检测是否为字符串
if (typeof name === "string") {
anotherName = name.substring(3);
}
// 检测是否为数字
if (typeof count === "number") {
updateCount(count);
}
// 检测是否为布尔值
if (typeof found === "boolean" && found) {
message("Found!");
}
// 检测是否为undefined
if (typeof MyApp === "undefined") {
MyApp = {
// 其他代码
};
}
typeof操作符是唯一的可以用来检测一个未声明变量但又不报错的方法.未声明的变量和值为undefined的变量在typeof的作用下都会返回“undefined”.
最后一种原始值类型, null,平常不应该检测值是否为该类型. 如前面所说,和null简单的做比较通常并不会给你足够的信息来判断该值是否为所期待的类型.不过有一个特例:如果所期待的类型就是null,那么这样做是可以的.比较时应该使用 === 或 !== .例如:
// 如果必须检测是否为null,则正确的做法是:
var element = document.getElementById("my-div");
if (element !== null) {
element.className = "found";
}
如果所查询的DOM元素不存在,则document.getElementById()会返回null.该方法只可能返回null或一个元素,由于null是可能的返回值之一,所以可以使用!==来检测返回的值是否为null.
使用typeof操作null会返回“object”,所以如果你必须要检测是否null,则使用恒等操作符 (===) 或者不恒等操作符 (!==)来检测.
检测引用值
引用值也称为对象值.在JavaScript中,所有非原始值的值都是引用值.有几种内建的引用类型比如Object, Array,Date, 和 Error,等.typeof操作符对于引用类型的值来说没有多大用处,
因为所有的对象类型都会返回同样的字符串“object”:
console.log(typeof {}); // "object"
console.log(typeof []); // "object"
console.log(typeof new Date()); // "object"
console.log(typeof new RegExp()); // "object"
另一个在对象值上使用typeof的缺点就是typeof在操作null时也返回“object”:
console.log(typeof null); // "object"
这种怪异的行为,已被确认为规范的严重bug,所以在想要精确的检测变量类型是否为null时,不要使用typeof.
instanceof操作符是用来检测某个值是否为一个特定的引用类型值的最好的方法.instanceof的语法如下:
value instanceof constructor
下面是一些例子
// 检测是否为一个日期对象
if (value instanceof Date) {
console.log(value.getFullYear());
}
// 检测是否为一个正则对象
if (value instanceof RegExp) {
if (value.test(anotherValue)) {
console.log("Matches");
}
}
// 检测是否为一个错误对象
if (value instanceof Error) {
throw value;
}
instanceof的一个很有趣的特性是它不仅检测某个值是否为创建这个对象的构造函数,还会检查原型链.原型链上包含了创建这个对象的继承信息.比如,每个对象默认都继承于Object,所以每个对象在使用instanceof Object进行检测时,都返回true.例如:
var now = new Date();
console.log(now instanceof Object); // true
console.log(now instanceof Date); // true
由于这样的特性,通常来说,如果你想要检测一个值是否为某个特定类型的对象,并不能使用value instanceof Object这样的方式.
instanceof操作符同样可以使用在用户自定义类型的对象上.例如:
function Person(name) {
this.name = name;
}
var me = new Person("Nicholas");
console.log(me instanceof Object); // true
console.log(me instanceof Person); // true
这个例子创建了一个自定义的Person类型.变量me是Person的一个实例,所以语句me instanceof Person会返回true.正如前面提到的,所有的对象都是Object的实例,所以me instanceof Object也返回true.
在JavaScript中,instanceof操作符是唯一的用来检测自定义类型对象的方法.对于大部分JavaScript内置类型的对象检测来说,也是很适合的.但是,有一个很严重的限制是:
假设一个对象来自于一个浏览器的框架(frame A),它被传递到另一个框架里 (frame B).两个框架都定义了Person这个构造函数.如果来自frame A的这个对象是frame A中Person构造函数的实例,则下面的规则成立:
// true
frameAPersonInstance instanceof frameAPerson
// false
frameAPersonInstance instanceof frameBPerson
因为每个框架都有自己私有的Person, 这个对象实例仅仅是他所在的那个框架的Person的实例,即使两个框架中定义的Person是完全相同的.
这种问题并不是只适用于用户自定义类型,对于JavaScript内置的两种重要类型也同样适用:函数和数组.不过对于这两种类型来说,你根本不需要用到instanceof.
检测函数
在JavaScript中,从技术上讲,函数也是引用类型的值,并且所有函数都是Function构造函数的实例.例如:
function myFunc() {}
// Bad
console.log(myFunc instanceof Function); // true
但是,这个方法在跨框架的情况下并不好用,因为每个框架都有自己的Function构造函数.幸运的是,typeof操作符同样也适用于函数,可以返回字符串“function”:
function myFunc() {}
// Good
console.log(typeof myFunc === "function"); // true
使用typeof是检测函数的最好方式,因为在跨框架的情形下它同样可用.只有一个特例:在IE8及之前版本中,在使用typeof检测任何DOM方法(比如document.getElementById())时,会返回“object”,而不是“function”.比如:
// IE8以及之前版本
console.log(typeof document.getElementById); // "object"
console.log(typeof document.createElement); // "object"
console.log(typeof document.getElementsByTagName); // "object"
这种怪异表现是由浏览器对DOM的实现机制导致的.简而言之,早期版本的IE没有将DOM实现为原生的JavaScript函数,这就导致了原生的typeof操作符将DOM函数识别为对象.由于这个原因,开发者们通常使用in操作符来检测DOM的某个功能特性,如果存在这个属性,也就意味着存在这个函数,例如:
// 检测DOM方法
if ("querySelectorAll" in document) {
images = document.querySelectorAll("img");
}
该代码检测了querySelectorAll是否在document中定义,如果是,则使用该函数.虽然并不是特别理想,但这是检查一个DOM方法的存在性的最安全的方法了. 在其他情况下,使用typeof操作符检测函数是最好的.
检测数组
在多个框架之间传递数组类型的值是由跨框架而引起的问题之一.开发者们发现:array instanceof Array并不是总能返回正确的结果.正如前面提到的,每个框架都有自己的Array构造函数,另一个框架来的数组对象不会识别这个框架的Array构造函数.Douglas Crockford首先建议使用"鸭子类型"来推断,检测 sort()方法的存在性:
// 鸭子类型
function isArray(value) {
return typeof value.sort === "function";
}
该检测主要是依据了数组是唯一的拥有sort()方法的内置对象类型.当然,如果传入一个拥有sort方法的自定义对象,该方法也会返回true.
在JavaScript中,有很多种检测方法用来尝试准确的检测一个值是否为数组类型,最终, Juriy Zaytsev (也被称为Kangax)提出了这个问题的一个优雅的解决方案:
function isArray(value) {
return Object.prototype.toString.call(value) === "[object Array]";
}
}
Kangax发现:在某个值上调用原生的toString()方法,所有浏览器上都会返回一个标准统一的字符串. 对于数组值来说,这个字符串是“[object Array]”,这个方法在跨框架的情况下也同样正常工作. Kangax的方法迅速流行起来,目前大部分JavaScript库都使用了这个方法.
这种方法通常用于检测原生对象,而不是开发人员自定义的对象.例如,原生的JSON对象使用这项技术时返回“[object JSON]”.
从那时起, ECMAScript 5正式引入了一个新的方法Array.isArray().这个方法的唯一目的就是为了准确地判断一个值是否为数组.正如Kangax的函数, Array.isArray()可以正确的处理那些跨框架的变量,所以许多JavaScript库都使用了下面类似的代码:
function isArray(value) {
if (typeof Array.isArray === "function") {
return Array.isArray(value);
} else {
return Object.prototype.toString.call(value) === "[object Array]";
}
}
Array.isArray()方法在Internet Explorer 9+, Firefox 4+, Safari 5+, Opera 10.5+, 和 Chrome中实现.
检测属性
另外一个某些开发者经常使用null和undefined的地方就是在尝试判断一个对象上某个属性的存在性的时候.例如:
// Bad: 属性值可能为假值
if (object[propertyName]) {
// 其他代码
}
// Bad: 和null做比较
if (object[propertyName] != null) {
// 其他代码
}
// Bad: 和undefined做比较
if (object[propertyName] != undefined) {
// 其他代码
}
上面的每个示例其实都是检查了给定名字的属性的属性值是否为真,而不是检查给定名字的属性是否存在.这可能导致:如果所检查的属性的值为0,空字符串, false,null,和undefined其中的一个.判断结果总为false.毕竟一个属性的属性值可以为假值.所以上面的代码都可能引起bug.
检测一个属性是否存在的最好的方法是使用in操作符.in操作符可以简单的检测一个已知名字的属性的存在性而不用读取这个属性的值,这样可以避免歧义.如果该属性存在于这个对象实例本身或者从该对象实例的原型链上继承而来,则in操作符返回true.例如:
var object = {
count: 0,
related: null
};
// Good
if ("count" in object) {
// 将会执行到这里
}
// Bad: 0为假值
if (object["count"]) {
// 不会执行到这里
}
// Good
if ("related" in object) {
// 将会执行到这里
}
// Bad: 和null做比较
if (object["related"] != null) {
// 不会执行到这里
}
如果你只要检测一个对象实例中某个属性的存在性,使用hasOwnProperty()方法.所有的JavaScript对象都从Object上继承了这个方法.当检测的属性是对象实例自身的属性时,该方法返回true (如果检测的属性是从那个原型链上继承而来的,则该方法返回false).注意IE8或者更早版本的IE中,DOM对象不继承Object,因此也就没有hasOwnProperty()方法.这意味着如果你想在DOM对象上使用hasOwnProperty()方法,必须先检查该方法的存在性.
// 适合所有非DOM对象
if (object.hasOwnProperty("related")) {
//其他代码
}
// 当你不确定object是否为DOM对象
if ("hasOwnProperty" in object && object.hasOwnProperty("related")) {
//其他代码
}
为了兼容IE8及更早版本的IE,我尽可能使用in操作符,并且只在需要确认是否是自身属性时才使用hasOwnProperty().
只要你想检测一个属性的存在性,确保使用了in操作符或者hasOwnProperty()方法.这样做可以避免很多bug.
第十二章
浏览器检测
浏览器检测一直是WEB开发领域的热点话题之一,早在使用JavaScript进行浏览器检测之前,浏览器检测就已经随着网景导航者(Netscape Navigator)的诞生而出现了.网景导航者是第一个真正流行并且被广泛使用的浏览器,网景导航者2.0在当时从技术上大大领先了所有其他可用的浏览器,所以网站们开始根据网景导航者独有的user-agent字符串返回只有该浏览器才能支持的内容.这种情形迫使其他浏览器开发商,尤其是微软,在他们各自浏览器的user-agent字符串中添加合适的字符串来通过这种形式的浏览器检测.
User-Agent 检测
最早的浏览器检测形式是user-agent检测,这种检测是指服务器(目前客户端也可以)通过查看一个HTTP请求中包含的user-agent字符串来判断所使用的浏览器.在当时,服务器仅仅通过user-agent字符串就阻止一些浏览器访问一个网站上的任何页面.这时,受益最大的浏览器就是网景导航者,因为该浏览器是当时最先进的浏览器,所以网站们认为只有该浏览器才能正常的显示网站的页面内容.网景导航者的user-agent字符串是这样的:Mozilla/2.0 (Win95; I)
当IE首个版本发布时,它实际上是被迫在自己的user-agent字符串中包含了网景的浏览器中的user-agent字符串中的大部分内容,这样才能确保已有的网站能允许IE这个新浏览器的正常访问,因为大部分user-agent检测都是在寻找字符串"Mozilla"以及斜杠后面的版本号的.IE的user-agent字符串是这样的:Mozilla/2.0 (compatible; MSIE 3.0; Windows 95)
这样一来,所有的user-agent检测都会把IE这个新浏览器当成网景浏览器来看待.从那开始,所有新的浏览器都会像IE那样,在自己的user-agent字符串中,包含已有浏览器的user-agent字符串中的一部分,一直到最新的浏览器Chrome也是这样, Chrome的user-agent字符串包含了Safari的其中一部分,从而也就包含了Firefox的user-agent字符串中的一部分,从而也就包含了网景浏览器的user-agent字符串中的一部分.
到了2005年,JavaScript开始越来越受欢迎.浏览器的user-agent字符串变的不光在服务器端能够获取到,通过JavaScript中的Navigator.userAgent属性也同样可以获取了.User-agent字符串检测开始从服务器端转移到了客户端的网页中检测,像这样:
// Bad
if (navigator.userAgent.indexOf("MSIE") > -1) {
// 该浏览器是IE
} else {
// 该浏览器不是IE
}
随着越来越多的网站开始使用JavaScript进行user-agent检测,出现了一部分网站因检测错误而无法正常显示的情况.十年前在服务器端出现的相同问题如今以JavaScript错误的形式出现在了客户端.
出现问题的最大原因就是解析user-agent字符串是很困难的,因为浏览器们都尝试复制另外的浏览器的user-agent字符串来确保兼容性.多一个新的浏览器,user-agent检测代码就得更新或重写,并且,从新的浏览器发布到检测代码更新的这一段事件内,会有一部分未知人数的用户在该网站得到不好的用户体验.
但这并不是说没有一种适合的方式来有效的使用user-agent字符串.存在一些兼容性很好的库,用在JavaScript或者服务器端的都有,它们实现了一种合理的检测机制.不幸的是,这些库也需要定期更新以跟上已有浏览器的更新进度和新浏览器的发布,但总体上要实现的目标就是不需要自己长期的维护.
User-agent检测永远是确保JavaScript正常运行的最后的办法.如果你选择了user-agent检测,则最安全的检测方式是只检测旧的浏览器.例如,如果你需要只在IE8和更早的IE版本下执行特定的代码,则你只需要检测用户所使用的是IE8以及之前版本而不要检测是IE9或更新的版本,如下所示:
if (isInternetExplorer8OrEarlier) {
// IE8或更早的版本
} else {
// 所有其他的浏览器
}
这样做的好处是:因为IE8以及更早的浏览器的user-agent字符串是众所周知的并且不会改变的.所以即使你的代码继续运行到了IE25发布的那天,同样的代码很可能不用进一步修改就能正常使用.反过来就不是了,如果你尝试检测IE9或者更新的浏览器,你会不得不经常更新自己的代码以确保兼容性.
浏览器并不总是报告他们的原本的user-agent字符串.几乎所有的浏览器上都有现成的User-agent切换工具.开发者经常会因考虑到这个原因而不敢使用user-agent检测,即便已经没有其他解决办法的情况下,他们考虑的是,很可能用户的user-agent是冒名顶替的.我的建议是不用去担心user-agent伪装.如果一个用户有能力去伪装他的user-agent字符串,则他也应该明白这样做可能会引起网站的异常.如果一个浏览器比如Firefox,他表现的并不像是Firefox,那这不是你的错.没有必要第二次尝试检测用户的user-agent字符串.
特性检测
为了使用一种更合理的检测浏览器的方法,开发者们开始使用一种称之为"特性检测"的技术.特性检测是指通过检测一个特定浏览器的特性,并且只在它可用是才使用该特性.所以你不应该使用下面的检测方法:
// Bad
if (navigator.userAgent.indexOf("MSIE 7") > -1) {
// 进行一些操作
}
你应该这样做:
// Good
if (document.getElementById) {
// 进行一些操作
}
这两种检测方法有一定的区别.第一种方法是通过一个浏览器的名称和版本号检测,而第二种方法是通过指定的特性来检测,比如document.getElementById方法.user-agent嗅探的结果是确切的知道用户所使用的浏览器类型以及版本号,而特性检测是判断一个指定的对象或者方法是否可用.注意这两种方法可能有完全不同的结果.
因为特性检测并不依赖用户使用的是哪个浏览器,只依赖某个特性是否可用,用来确保兼容性. 比如,当DOM还未成熟时,并不是所有的浏览器都支持document.getElementById(),所以存在很多下面的类似代码:
// Good
function getById (id) {
var element = null;
if (document.getElementById) { // DOM
element = document.getElementById(id);
} else if (document.all) { // IE
element = document.all[id];
} else if (document.layers) { // Netscape <= 4
element = document.layers[id];
}
return element;
}
上面的例子很恰当的使用了特性检测,首先检测一个指定的特性,如果存在该特性,则使用它.首先检测document.getElementById(),是因为它是基于标准的解决方法.然后是两个浏览器私有的解决方法.如果所有特性都不可用,则该函数简单的返回一个null.该函数最好的地方是:当支持document.getElementById()的IE 5和网景6发布时,该代码不需要做任何改变.上面的例子演示了一个好的特性检测的几个重要组成部分:
1. 首先测试标准的解决方案
2. 然后测试浏览器私有的解决方案
3. 如果没有解决方案可用,则提供一个逻辑上的回滚
同样的方法也适用在下面这种情况下:当一个新的特性成为正式标准时,某些浏览器已经早已实验性的实现了同样的方法.只是带有各浏览器特定的前缀. 例如,requestAnimationFrame()方法是在2011年末正式成为标准,在那之前,有几个浏览器已经实现了他们各自的带有开发商前缀的私有版本的方法.合适的对requestAnimationFrame()方法进行特性检测的代码如下:
// Good
function setAnimation (callback) {
if (window.requestAnimationFrame) { // standard
return requestAnimationFrame(callback);
} else if (window.mozRequestAnimationFrame) { // Firefox
return mozRequestAnimationFrame(callback);
} else if (window.webkitRequestAnimationFrame) { // WebKit
return webkitRequestAnimationFrame(callback);
} else if (window.oRequestAnimationFrame) { // Opera
return oRequestAnimationFrame(callback);
} else if (window.msRequestAnimationFrame) { // IE
return msRequestAnimationFrame(callback);
} else {
return setTimeout(callback, 0);
}
}
上面的代码首先检测标准的requestAnimationFrame()方法,只有当该方法不存在的时候,采取继续检测其他浏览器的私有实现.最后一个选项,是为那些不支持任何类似方法的浏览器准备的,使用setTimeout()方法来模拟.这样的话,即使浏览器更新,废弃掉自己带有前缀的方法名之后,上面的代码同样不需要任何改动.
避免特性推断
一个不合理的使用特性检测的方式称之为特性推断.特性推断是指:在得知某一个特性存在后,就尝试使用其他未检测过的多个特性.这些未检测过的特性的可用性是由另外一个检测通过的特性推断出来的.所以问题就出来了,推断毕竟是推断,而不是事实,即使脚本在目前能够正常运行,也很有可能在未来出一些问题.
例如,下面就是一个使用特性推断的较老的代码:
// Bad - 使用了特性推断
function getById (id) {
var element = null;
if (document.getElementsByTagName) { // DOM
element = document.getElementById(id);
} else if (window.ActiveXObject) { // IE
element = document.all[id];
} else { // Netscape <= 4
element = document.layers[id];
}
return element;
}
该函数的特性推断是很糟糕的,一共有这样几个推断:
• 如果document.getElementsByTagName()可用,则document.getElementById() 也可用.从本质上讲,该推断是从一个DOM方法的存在性推导出所有的DOM方法都可用.
• 如果window.ActiveXObject可用,则document.all可用.这个推断基本上是说,window.ActiveXObject只在IE中可用,而document.all也是只在IE中可用,所以只要知道其中一个存在,则能推导出另外一个也必然存在.然而实际上,一些版本的Opera浏览器也支持document.all.
• 如果所有的推断都没有通过,则该浏览器必然是网景导航者4或者更早的浏览器.这也并不是严格正确的.
你不能通过一个特性的存在就推导出另一个特性也存在.两个特性之间的关系如果是细微的则是最好的,如果是间接相关的则是最坏的.用一句俚语来说,就是“如果它看起来像一只鸭子,那么它必须叫起来也得像一只鸭子,才是真的鸭子”.
避免浏览器推断
不知从什么时候开始,许多web开发者开始混淆了user-agent检测与特性检测之间的区别.于是开始出现如下类似的代码:
// Bad
if (document.all) { // IE
id = document.uniqueID;
} else {
id = Math.random();
}
上述代码的问题是:检测document.all其实是在隐式的检测是否为IE.一旦知道该浏览器是IE,则假设使用document.uniqueID是安全的,因为它是IE私有的.但是,你的检测只是判断了document.all是否可用,并没有判断浏览器是否为IE.仅仅因为document.all可用并不能推断出document.uniqueID也是可用的.如果存在一个非IE的浏览器也支持document.all,则该代码就会出错了.
还有一个更简明的错误用法,就是开发者们开始抛弃下面的代码:
var isIE = navigator.userAgent.indexOf("MSIE") > -1;
而使用这条语句:
// Bad
var isIE = !!document.all;
这样来改变自己的代码表明了这些开发者误解了“不要使用user-agent检测.”这句话.这样的操作不是检测是否为特定的浏览器,而是通过检测是否存在否个特性来尝试推断出是否为那个特定的浏览器,这样做是很不对的.这样的行为就称之为浏览器推断,是一种很不好的方式.
不知从什么时候开始,开发者们开始意识到使用document.all并不是检测一个浏览器是否为IE的最好的办法.所以上面的代码又被修改为如下所示:
// Bad
var isIE = !!document.all && document.uniqueID;
这种方法未免“太过聪明”了.写代码的人正在艰难的尝试使用越来越多的特性来判断一些东西.更糟糕的是,别的浏览器完全有可能实现都相同的特性, 这会最终导致该代码产生错误的结果.
甚至在一些JavaScript库中也出现了浏览器推断的代码.下面的代码片段来自MooTools 1.1.2:
// from MooTools 1.1.2
if (window.ActiveXObject)
window.ie = window[window.XMLHttpRequest ? 'ie7' : 'ie6'] = true;
else if (document.childNodes && !document.all && !navigator.taintEnabled)
window.webkit = window[window.xpath ? 'webkit420' : 'webkit419'] = true;
else if (document.getBoxObjectFor != null || window.mozInnerScreenX != null)
window.gecko = true;
该代码尝试通过浏览器推断来判断出所使用的浏览器.这样会产生下面的问题:
• IE8,既支持window.ActiveXObject,也支持window.XMLHttpRequest,所以会被错误识别为IE7.
• 任何实现了document.childNodes的浏览器,只要没被识别为IE,就会被识别为WebKit.
• WebKit的版本号识别有问题,WebKit 422或者更高的版本都会被错误的识别为WebKit 420.
• 没有检测Opera,所以Opera必然会被错误的检测为其他浏览器,或者任何浏览器都不是.
• 该代码在新浏览器发布时必然需要更新.
上面的代码中,因浏览器推断而引起的问题的数量如此之多,尤其是最后一条.每当一个新浏览器发布时,MooTools必须修复他们的代码并且很快的发布出去,这样才能尽量避免错误的浏览器推断.很显然,这样的代码不适合长期维护.
想要知道为什么浏览器推断是应该避免的,你只需要回想一下高中时讲条件命题的那堂数学课,条件命题是由一个假设(p)和一个结论(Q)组成的,形式为“如果p为真,则能推出q为真.”你可以尝试修改这个命题来改变它的真假.有三种修改方法:
• 逆命题:如果q为真,则能推出p为真
• 否命题:如果p不为真,则能推出q也不为真
• 逆否命题:如果q不为真,则能推出p也不为真
在各种形式的命题中存在两种重要的推导关系,一个是:如果原命题是真的,则其逆否命题也是真的.例如,如果原命题为“如果它是一辆汽车,则它一定有轮子”(真命题),则逆否命题,“如果它没有轮子,则它一定不是一辆汽车”(也是真命题).
第二个推导关系是:逆命题和否命题之间,如果一个是真的,则另外一个也必然是真的.这在逻辑上是讲的通的,因为逆命题和否命题的关系和原命题与逆否命题的关系一样,都是互为逆否命题.
也许比这两个推导关系更需要知道的是那些不存在的关系.如果原命题为真,并不能确保其逆命题为真.因此浏览器推断是靠不住的.比如存在这个真命题:“如果它是IE,则document.all是可用的.” 逆否命题为,“如果document.all不可用,则它肯定不是IE”也是真命题.逆命题为,“如果document.all可用,则它就是IE”为假命题(一些版本的Opera也实现了document.all).基于特性的推断是基于"逆命题也是正确的"这个假设,而实际上并不是这样的.
添加多个结论也不会起到什么帮助,继续举这个例子,“如果它是一辆汽车,则它一定有轮子”,逆命题显然错误:“如果它有轮子,则它一定是汽车”,你可以更精确的讲:“如果它是一辆汽车,则它一定有轮子并且需要燃料”,逆命题为:“如果它有轮子并且需要燃料,则它一定是一辆车”,很显然也是不对的,因为飞机也符合.那么再试一次:“如果它是一辆汽车,则它一定有轮子并且需要燃料并且有两个车轴”,该命题的逆命题仍然是不对的.
问题的本质其实是一个人类语言的本质:想要通过一些特征的集合来决定全局是不行的.我们有"车"这个单词,它代表了车这个物体所有方面的特性,这样一来你就不需要用一大堆语言来列举出这些特性来说明白你上班是开这个东西去的.尝试通过使用越来越多的特性来判断浏览器也会有同样的问题.你只能越来越接近正确结果,但它绝不可靠.
因为使用了基于特性的浏览器检测,MooTools把自己和它的用户逼到了角落里.Mozilla从Firefox 3开始就警告用户,getBoxObjectFor()方法已经废弃,且在未来版本中会被删除.由于MooTools依赖这个方法来判断一个浏览器是否为为Gecko内核,所以Mozilla在Firefox 3.6中删除了这个方法就意味着所有使用MooTools老版本的页面都会受到影响.这种情况促使MooTools发布了更新新版本的建议:具体内容为:
我们已经将原先的浏览器检测修改为基于user agent字符串检测的方法.该方法也是目前所有JavaScript库的标准做法,正像前段时间Firefox 3.6上发生的问题一样.浏览器们变的越来越一样,想要通过一些特性来区分他们会变得越来越难且风险巨大.从现在开始,浏览器检测仅仅会在必须使用的情况下才会使用,为了能让这个全球范围内使用JavaScript库的用户体验到兼容不同浏览器的一致体验.
应该使用什么?
特性推断和浏览器推断是非常不好的做法,应该不惜一切代价的禁止使用.直接的特性检测是最好的做饭,并且几乎在所有情况下,得到的结果都是你所希望的.通常来说,你只需要在使用一个特性之前,知道它是否可用即可.不要尝试推断一个特性和其他特性之间的关系,因为你最终会得到错误的答案.
我并不会很极端的说:不要使用user-agent检测,因为我相信还是有比较合理的方式来使用该检测的.但并不是说,有许多适合使用该检测的地方.如果你想使用user-agent嗅探,记住了:唯一安全的做法就是只检测旧版本的浏览器,而不要尝试检测是否为当前的新版浏览器甚至是未来版本的浏览器.
我的建议是经可能的使用特性检测.如果不行,则使用user-agent检测.绝不能使用浏览器推断,因为这样的代码是不适合维护的,会需要经常频繁的更新.
附录A
JavaScript 风格指南
编程语言的风格指南对于确保一个软件的长期可维护性来说是非常重要的,该指南基于Java语言编码规范和Douglas Crockford写的JavaScript语言编码规范所作,且根据我的个人经验和喜好进行了一些修改.
缩进
每个缩进级别是由四个空格组成的,不要使用制表符.
// Good
if (true) {
doSomething();
}
行长度
每一行代码的字符数不应该超过80.如果一行代码已经超过了80个字符,则应该在该行代码中某个合适的操作符(逗号,加号等)之后进行换行,操作符之后的一行代码应该使用两个级别的缩进(8个字符).
// Good
doSomething(argument1, argument2, argument3, argument4,
argument5);
// Bad: 操作符之后的换行只有四个空格的缩进
doSomething(argument1, argument2, argument3, argument4,
argument5);
// Bad: 在操作符之前进行了换行
doSomething(argument1, argument2, argument3, argument4
, argument5);
原始值字面量
字符串应该始终使用双引号来包裹(不要使用单引号),且应该始终出现在一行内,不要使用反斜杠在字符串内部进行换行.
// Good
var name = "Nicholas";
// Bad: 使用了单引号
var name = 'Nicholas';
// Bad: 使用反斜杠进行了换行
var longString = "Here's the story, of a man \
named Brady.";
数字应该表示为十进制整数,科学记数法整数,十六进制整数,或小数点前后至少各一位的浮点小数,切勿使用八进制字面量。
// Good
var count = 10;
// Good
var price = 10.0;
var price = 10.00;
// Good
var num = 0xA2;
// Good
var num = 1e23;
// Bad: 末尾小数点后面没有数字
var price = 10.;
// Bad: 开头的小数点前面没有数字
var price = .1;
// Bad: 八进制数字已被废弃(ES5严格模式不可用)
var num = 010
特殊字面量null,应该只在下面列举的几种情形下使用:
• 初始化一个变量,且该变量随后可能会被赋值为一个对象值
• 和一个值可能是或可能不是对象值的已赋值的变量进行比较
• 传递给一个所需参数的类型为对象的函数
• 在需要一个返回值为对象类型的函数内部作为返回值
// Good
var person = null;
// Good
function getPerson() {
if (condition) {
return new Person("Nicholas");
} else {
return null;
}
}
// Good
var person = getPerson();
if (person !== null){
doSomething();
}
// Bad: 与未初始化的变量进行比较
var person;
if (person != null){
doSomething();
}
// Bad: 检测一个形参是否被传入实参
function doSomething(arg1, arg2, arg3, arg4){
if (arg4 != null){
doSomethingElse();
}
}
永远不要使用特殊字面量undefined.想要检测一个变量是否被定义,使用typeof操作符:
// Good
if (typeof variable == "undefined") {
// 其他代码
}
// Bad: 使用了undefined字面量
if (variable == undefined) {
// 其他代码
}
运算符间距
具有两个操作数的运算符(二元运算符)必须在左右两边各有一个空格,这样才能使整个表达式清晰.运算符包括赋值运算符和逻辑运算符.
// Good
var found = (values[i] === item);
// Good
if (found && (count > 10)) {
doSomething();
}
// Good
for (i = 0; i < count; i++) {
process(i);
}
// Bad: === 两边缺少空格
var found = (values[i]===item);
// Bad: && 和 > 两边缺少空格
if (found&&(count>10)) {
doSomething();
}
// Bad: == 和 < 两边缺少空格
for (i=0; i<count; i++) {
process(i);
}
括号间距
在使用小括号时,开始括号之后和结束括号之前的位置不应该有空白.
// Good
var found = (values[i] === item);
// Good
if (found && (count > 10)) {
doSomething();
}
// Good
for (i = 0; i < count; i++) {
process(i);
}
// Bad: 开始括号之后有额外的空白
var found = ( values[i] === item);
// Bad: 结束括号之前有额外的空白
if (found && (count > 10) ) {
doSomething();
}
// Bad: 参数两边具有额外的空白
for (i = 0; i < count; i++) {
process( i );
}
对象字面量
对象字面量应具有以下格式:
• 开始大括号应当与所在的语句处于同一行.
• 每个属性-值对应当与开始大括号后面的第一个属性具有同一级别的缩进.
• 每个属性-值对应当有一个不加引号的属性名,随后是一个冒号(冒号前面没有空格,后面有),然后是属性值.
• 如果属性值是一个函数,则该函数应当在函数体开始之后进行换行,并且该属性-值对的上下两侧都应该留一空行
• 为了增强可读性或者存在一组互相关联的属性,则可以在合适的位置插入空行来分割它们.
• 结束大括号应当处于单独的一行.
// Good
var object = {
key1: value1,
key2: value2,
func: function() {
// 其他代码
},
key3: value3
};
// Bad: 不恰当的缩进
var object = {
key1: value1,
key2: value2
};
// Bad: 属性值为函数的上下两边缺少空行
var object = {
key1: value1,
key2: value2,
func: function() {
// 其他代码
},
key3: value3
};
当一个对象字面量作为一个函数的实参时,开始大括号应当与函数名处于同一行,好像该对象是一个变量一样,上面介绍的其他对象字面量的格式规则也同样适用.
// Good
doSomething({
key1: value1,
key2: value2
});
// Bad: 整个对象字面量都处于同一行
doSomething({ key1: value1, key2: value2 });
注释
要频繁的使用注释,以帮助其他人理解你的代码.在以下列举的几种情形下,都应当使用注释:
• 代码很复杂以至于难以理解.
• 代码可能会被误认为是一个错误.
• 只针对特定浏览器的代码,但不容易被发现.
• 需要自动生成文档的对象,方法,或者属性(使用合适的文档注释).
单行注释
单行注释应该用来为单行的代码或者一组相关的代码行添加注释.单行注释可以用在下面三种情形下:
• 单独占一行,用来给下面的一行代码添加注释.
• 放在一行代码的末尾,用来给该行代码添加注释.
• 处于多行代码的行首处,用来注释掉这些代码行.
当单行注释处于单独的一行时, 该注释行应该与它所注释的代码具有同一级别的缩进,并且它的上面应该有一个空行.不要使用连续的多行单行注释,应该使用多行注释来替代.
// Good
if (condition){
// 如果代码运行到这里,则所有的安全性检查都已经检测通过
allowed();
}
// Bad: 在注释行前面没有空行
if (condition){
// 如果代码运行到这里,则所有的安全性检查都已经检测通过
allowed();
}
// Bad: 错误的缩进
if (condition){
// 如果代码运行到这里,则所有的安全性检查都已经检测通过
allowed();
}
// Bad: 应该使用多行注释
// 下面的代码非常复杂,所以我来解释一下吧.
// 你想要做的是确保只有在变量condition为真的时候,
// 才允许用户登录,变量condition的值是由几个不同的函数
// 计算出来的,并且它在当前session的生命周期内可能发生变化.
if (condition){
// 如果代码运行到这里,则所有的安全性检查都已经检测通过
allowed();
}
对于处于一行代码末尾的单行注释来说,必须确保代码行末尾和该单行注释之间最少有一个级别的缩进.
// Good
var result = something + somethingElse; // somethingElse的值不能为null
// Bad: 代码和单行注释之间没有缩进
var result = something + somethingElse;// somethingElse的值不能为null
唯一一种可以使用连续多行的单行注释的地方是用来注释掉大段代码的时候,多行注释不适合用在此处.
// Good
// if (condition){
// doSomething();
// thenDoSomethingElse();
// }
多行注释
多行注释用来为那些需要大量解释的代码行添加注释.每个多行注释应当至少有三行:
1. 第一行只包含一个注释开始的标记 /* ,该行中不允许有其他的文字.
2. 第二行开始的其他行应该以 * 开头,并且要与第一行中的 * 保持对齐,这些行中可以填写注释内容.
3. 最后一行只包含一个注释结束标记 */,并且其中的 * 要与第一行中的 * 保持对齐.该行中不允许有其他的文字.
多行注释的第一行应该与它所注释的代码行拥有同一级别的缩进.随后的代码在此缩进的基础上再增加一个空格 (为了对齐行首的 * 字符).每个多行注释上方应该有一个空行.
// Good
if (condition){
/*
* 如果代码运行到这里,
* 则所有的安全性检查都已经检测通过
*/
allowed();
}
// Bad: 注释前面缺少空行
if (condition){
/*
* 如果代码运行到这里,
* 则所有的安全性检查都已经检测通过
*/
allowed();
}
// Bad: 每行注释的星号后面都缺少空格
if (condition){
/*
*如果代码运行到这里,
*则所有的安全性检查都已经检测通过
*/
allowed();
}
// Bad: 错误的缩进
if (condition){
/*
* 如果代码运行到这里,
* 则所有的安全性检查都已经检测通过
*/
allowed();
}
// Bad: 在代码行末尾的注释不要使用多行注释
var result = something + somethingElse; /*somethingElse的值不能为null*/
批注型注释
注释可以用来为若干行代码添加额外的批注信息.这些批注都是由一个单词后跟一个冒号的形式打头.常用的批注型注释有以下几种:
TODO
表明该代码还没有完成,还应该说明接下来下一步该做什么.
HACK
表明该代码使用了某种捷径,还应该说明为什么要使用这个hack.这个批注也隐含的说明了:如果有一个其他非hack的方法来解决这个问题的话,会更好.
XXX
表明该代码是有问题的,应尽快修复.
FIXME
表明该代码是有问题的,要及时修复.重要程度小于XXX.
REVIEW
表明该代码需要审查,以找出潜在的问题.
这些类型的批注可以用在单行注释和多行注释内,并且应该遵循所有普通注释应该遵循的规则.
// Good
// TODO: 我准备研究一下如何让下面的代码跑得更快点.
doSomething();
// Good
/*
* HACK: 只在IE上执行的代码.我计划在未来有时间的时候重审该处代码
* 该代码很可能会在版本v1.2之前被替换掉.
*/
if (document.all) {
doSomething();
}
// Good
// REVIEW: 有没有一个更好的方法来实现这个功能呢?
if (document.all) {
doSomething();
}
// Bad: 冒号前面多一个空格
// TODO : 我准备研究一下如何让下面的代码跑得更快点.
doSomething();
// Bad: 注释行应该与所注释的代码行有相同级别的缩进
//REVIEW:有没有一个更好的方法来实现这个功能呢?
if (document.all) { doSomething(); }
变量声明
所有的变量在使用之前必须先声明.变量声明语句应该放在一个函数体的最前面,并且每个函数体内只应该使用一个var语句.
var 语句中,每个变量声明应该独占一行. 第一行后面的所有行都应该使用一个级别的缩进,这样才能让所有的变量名对齐.
var 语句中,尽量在变量声明时就同时进行初始化,且不同行中变量初始化所使用的等号应该对齐.经过初始化的变量应该放在那些没有经过初始化的变量之前.
// Good
var count = 10,
name = "Nicholas",
found = false,
empty;
// Bad: 变量声明语句中等号没有对齐.
var count = 10,
name = "Nicholas",
found= false,
empty;
// Bad: 不正确的缩进
var count = 10,
name = "Nicholas",
found = false,
empty;
// Bad: 多个变量声明出现在同一行里
var count = 10, name = "Nicholas",
found = false, empty;
// Bad: 没有初始化赋值的变量放在了最前面
var empty,
count = 10,
name = "Nicholas",
found = false;
// Bad: 多个var语句
var count = 10,
name = "Nicholas";
var found = false,
empty;
在使用变量之前始终要先进行声明,不要使用隐式的全局变量.
函数声明
函数应该在使用之前进行声明.当一个函数不是方法时(也就是说,该函数不属于某个对象),则它应该使用"函数声明"形式来声明(而不是使用函数表达式或者使用Function构造函数).
在函数名和开始小括号之间不应该有空格,但结束小括号和开始大括号之间应该有一个空格.函数体开始时的开始大括号应该与function关键字处于同一行.
在包裹形参的开始小括号后面和结束小括号前面,不应该有空格.每个形参之间的逗号后面应该有一个空格,但前面没有.整个函数体应该缩进一个级别.
// Good
function doSomething(arg1, arg2) {
return arg1 + arg2;
}
// Bad: 函数名后面不应该存在空格
function doSomething (arg1, arg2){
return arg1 + arg2;
}
// Bad: 函数表达式
var doSomething = function(arg1, arg2) {
return arg1 + arg2;
};
// Bad: 左大括号不应该换行
function doSomething(arg1, arg2)
{
return arg1 + arg2;
}
// Bad: 不应该使用Function构造函数
var doSomething = new Function("arg1", "arg2", "return arg1 + arg2");
存在于函数内部的函数声明应该紧随在var声明语句之后.
// Good
function outer() {
var count = 10,
name = "Nicholas",
found = false,
empty;
function inner() {
// 其他代码
}
// 下面的代码可以调用inner()
}
// Bad: 内部函数不应该在变量声明语句之前
function outer() {
function inner() {
// 其他代码
}
var count = 10,
name = "Nicholas",
found = false,
empty;
// 下面的代码可以调用inner()
}
匿名方法可以作为一个对象的方法的赋值,也可以作为传入其他函数的参数.在function关键字和开始括号之间,不应该存在空格.
// Good
object.method = function() {
// 其他代码
};
// Bad: function关键字和(之间不应该有空格
object.method = function () {
// 其他代码
};
"立即调用函数"应该使用一对小括号包住整个函数调用.
// Good
var value = (function() {
// 其他代码
return {
message: "Hi"
}
}());
// Bad: 函数调用没有用小括号包住
var value = function() {
// 其他代码
return {
message: "Hi"
}
}();
// Bad: 小括号应该连()也一起包裹
var value = (function() {
// 其他代码
return {
message: "Hi"
}
})();
命名
能够合适的对变量以及函数进行命名是很重要的,命名应该只包含英文字母和数字,有些情况下也可以使用下划线.不要在任何命名中使用美元符($)和反斜杠(\).
变量名应该使用骆驼命名法,也就是说:首个单词的首字母要小写,并且第二个单词开始的首字母要大写. 一个变量名的首个单词应该是名词(而不是动词),这样才能避免它被误认为是一个函数名.不要在变量名中使用下划线.
// Good
var accountNumber = "8401-1";
// Bad: 不应该使用大写字母开头
var AccountNumber = "8401-1";
// Bad: 不应该使用动词作为第一个单词
var getAccountNumber = "8401-1";
// Bad: 命名中不应该使用下划线
var account_number = "8401-1";
函数名也应该使用骆驼命名法.函数名中的第一个单词应该使用一个动词(而不是名词),这样才能避免它被误认为是一个变量名.不要在函数名中使用下划线.
// Good
function doSomething() {
// 其他代码
}
// Bad: 不应该使用大写字母开头
function DoSomething() {
// 其他代码
}
// Bad: 需要一个动词
function car() {
// 其他代码
}
// Bad: 不应该使用下划线
function do_something() {
// 其他代码
}
构造函数(配合new操作符可以用来生成新对象的函数)的命名规则应该使用骆驼命名法,但首字母得大写.构造函数的名称应该使用一个非动词,因为new操作符才是用来创建一个类的对象实例的动作.
// Good
function MyObject() {
// 其他代码
}
// Bad: 不应该用小写字母开头
function myObject() {
// 其他代码
}
// Bad: 不应该使用下划线
function My_Object() {
// 其他代码
}
// Bad: 不应该使用一个动词
function getMyObject() {
// 其他代码
}
一个角色为常量(值不会被修改)的变量的名称应该全部使用大写字母,并且单词之间要使用下划线进行分割.
// Good
var TOTAL_COUNT = 10;
// Bad: 常量不应该使用骆驼命名法
var totalCount = 10;
// Bad: 常量不应该大小写混合
var total_COUNT = 10;
一个对象的属性名遵循和普通变量一样的命名规则.一个对象的方法名遵循和普通函数一样的命名规则.如果一个属性或者方法是私有的,则它们的命名应该以下划线开头.
// Good
var object = {
_count: 10,
_getCount: function () {
return this._count;
}
};
严格模式
严格模式应该仅在函数内部使用,不要在全局作用域中使用.
// Bad: 全局的严格模式
"use strict";
function doSomething() {
// 其他代码
}
// Good
function doSomething() {
"use strict";
// 其他代码
}
如果你想在多个函数中使用严格模式,又不想在每个函数中都写一遍 "use strict",则应该使用立即调用函数将这些函数包裹起来:
// Good
(function() {
"use strict";
function doSomething() {
// 其他代码
}
function doSomethingElse() {
// 其他代码
}
}());
赋值
当为一个变量赋值时,如果等号右边包含一个比较表达式,则应该使用小括号将该表达式括住.
// Good
var flag = (i < count);
// Bad: 缺少括号
var flag = i < count;
等号运算符
使用 === 和 !== 代替 == 和 != ,避免类型转换引起的错误.
// Good
var same = (a === b);
// Bad: 不应该使用 ==
var same = (a == b);
条件运算符
条件运算符应该仅用在条件赋值的时候,不应该用它来代替if语句.
// Good
var value = condition ? value1 : value2;
// Bad: 不是赋值语句,应该使用if语句
condition ? doSomething() : doSomethingElse();
语句
简单语句
每行代码应该最多只能包含一个语句. 所有的简单语句都应该以分号(;)结尾.
// Good
count++;
a = b;
// Bad: 多个语句处于同一行
count++; a = b;
return 语句
如果return语句返回一个返回值,则该返回值不需要使用小括号括住,除非在小括号可以让整个返回值语句显得更加清晰的情况下.比如:
return;
return collection.size();
return (size > 0 ? size : defaultSize);
复合语句
复合语句是被包裹在一个大括号中的多个简单语句.
• 符合语句中的每个语句应该根据嵌套层级增加一个或者多个级别的缩进.
• 开始大括号应该放在复合语句开始的那一行的末尾,结束大括号应该独占一行,并且缩进要与符合语句的开始行对齐.
• 被包括在一个控制结构语句(比如if语句或者for语句)中的语句一定要用大括号括起来,即使只有一条语句也是这样.这样的规则可以避免在添加语句的同时忘了添加大括号而引起的bug.
• 表示某个语句开始的关键字,比如if,后面应该添加一个空格,而且,复合语句开始的大括号前面也应该有一个空格.
if 语句
if 语句应该具有以下的形式:
if (condition) {
statements
}
if (condition) {
statements
} else {
statements
}
if (condition) {
statements
} else if (condition) {
statements
} else {
statements
}
绝不能省略if语句中任意一个可以省略的大括号
// Good
if (condition) {
doSomething();
}
// Bad: )和{之间缺少空格
if(condition){
doSomething();
}
// Bad: 缺少大括号
if (condition)
doSomething();
// Bad: 不应该写在一行上
if (condition) { doSomething(); }
// Bad: 不应该写在一行上,且缺少大括号
if (condition) doSomething();
for 语句
for语句应该具有以下的形式:
for (initialization; condition; update) {
statements
}
for (variable in object) {
statements
}
变量应该在for语句的代码初始化部分之前就进行声明.
// Good
var i,
len;
for (i=0, len=10; i < len; i++) {
// 其他代码
}
// Bad: 变量在初始化时才进行了声明
for (var i=0, len=10; i < len; i++) {
// 其他代码
}
// Bad: 变量在初始化时才进行了声明
for (var prop in object) {
// 其他代码
}
在使用for-in语句时,要仔细检查是否需要使用hasOwnProperty()方法来过滤掉那些原型链上层的对象元素.
while 语句
while语句应该具有以下的形式:
while (condition) {
statements
}
do 语句
do语句应该具有以下的形式:
do {
statements
} while (condition);
注意不要丢掉语句末尾的分号.且在while关键字后面要留一个空格.
switch 语句
switch语句应该具有以下的形式:
switch (expression) {
case expression:
statements
default:
statements
}
switch语句下面的每个case语句都应该有一个级别的缩进,且从第二个开始的case语句以及最后的default语句上方,都应该有一个空行.
每组case代码块(default除外)应该以break语句,return语句,throw语句,或者一个"falls through"注释来结尾.
// Good
switch (value) {
case 1:
/* falls through */
case 2:
doSomething();
break;
case 3:
return true;
default:
throw new Error("This shouldn't happen.);
}
如果一个switch语句没有default分支, 则应该添加一行注释.
// Good
switch (value) {
case 1:
/*falls through*/
case 2:
doSomething();
break;
case 3:
return true;
// no default
}
try 语句
try语句应该具有以下的形式:
try {
statements
} catch (variable) {
statements
}
try {
statements
} catch (variable) {
statements
} finally {
statements
}
空白
通过空白行可以剥离出一段逻辑相关的代码,以提高可读性。
两个连续空行应该在以下情形中使用:
• 在源代码中的两部分代码之间.
• 在类和接口声明之间.
空行应该在以下情形中使用:
• 在方法之间.
• 在一个方法内部的局部变量声明语句之前.
• 在一个多行或单行注释之前.
• 在一个方法中用来分割出一段逻辑相关的代码,以提高可读性.
空格应该在以下情形中使用:
• 在一个关键字和小括号之间应该添加一个空格.
• 在形参列表之间的逗号后面应该添加一个空格.
• 所有的二元运算符除了(.)之外,应该用空格将自身与它的操作数分隔开.一元运算符不应该使用空格分割,比如一元减号,自增(++),以及自减(--)运算符.
• for语句中的三个表达式中的分号后面应该添加空格.
避免使用的东西
• 永远不要使用原始值的包装类型来生成对象,比如String,Number等.
• 永远不要使用eval().
• 永远不要使用with语句.该语句在ES5中的严格模式中不可用,并且很有可能在未来版本的ECMAScript版本中被废弃.
附录B
JavaScript 工具
构建工具
虽然很多构建工具不是专门针对JavaScript的,但它们在管理一些大的JavaScript项目时仍然非常的有用:
我最喜欢的适用于JavaScript项目的构建系统,基于Java编写.
一个基于Node.js的构建系统,内置了一些与JavaScript以及CSS相关的任务的支持.
一个很老的构建系统,但在Unix用户中间还很流行.jQuery就使用了这一构建系统.
一个基于Node.js的构建系统,内置了一些与JavaScript相关的任务的支持,比如合并和压缩JavaScript.
一个基于Ruby的asset包装库,可以用来进行JavaScript和CSS代码的压缩合并等
一个基于Python的构建系统.
一个用Ruby编写的类似于Gmake的工具.Sass,一个很流行的CSS预处理程序,将会使用Rake工具来构建.
一个基于Rack的构建系统.
文档生成器
文档生成器用来根据源代码中的注释生成代码文档:
一个并排显示的文档生成器,在源代码旁边直接显示文档.用CoffeeScript编写.
Dojo的官方文档生成器. 用PHP编写.
一个JavaScript文档生成器,使用了Markdown语法.用JavaScript编写.
一个基于Java的文档生成器.最常用的文档生成器之一.
一个通用目的的适合多种语言的文档生成器.用Perl编写.
PDoc的一个JavaScript接口.
Prototype的官方文档生成器. 用Ruby编写.
YUI的文档生成器. 用JavaScript编写.
代码检查工具
代码检查工具用来帮助你在你的代码中找出代码模式和代码样式上的问题:
Douglas Crockford写的代码质量检测工具.
JSLint的分支版本,添加了更多的配置选项.
压缩工具
压缩工具可以通过移除所有不必要的注释和空白符的方法来缩小JavaScript文件的体积,有些压缩器还会进行一些其他的代码优化.
Google的基于Java的JavaScript压缩器.
一个基于Node.js的javascript压缩器.
一个基于Java的JavaScript 和 CSS 压缩器.
测试工具
测试工具允许你通过编写和执行测试代码,来验证你写的程序代码是否正确:
一个行为驱动的JavaScript测试框架.
Google的单元测试框架,可以进行全自动化的浏览器测试.
一个无界面的,主要用来进行代码测试的WebKit浏览器.默认可以与QUnit和Jasmine配合使用,还可以通过一个驱动系统与其他测试框架配合使用.
jQuery的单元测试框架.
用来进行浏览器测试的功能测试框架.
一个用来测试浏览器中运行的JavaScript代码的测试工具.
YUI的单元测试框架.