優秀なストヤン ステファノフ氏が、オライリーから初めて出版された彼の新著『JavaScript Patterns』で、読者のために要約を寄稿できるのは素晴らしいことだと思いました。具体的には、グローバル変数の回避、単一変数宣言の使用、ループ内の長さのプリキャッシュ、次のコード読み取りなど、高品質の JavaScript を記述するための要素がいくつかあります。
この概要には、コードにはあまり関連性がありませんが、API ドキュメントの作成、ピア レビューの実行、JSLint の実行など、コード作成全体に非常に関連するいくつかの習慣も含まれています。これらの習慣とベスト プラクティスは、数か月または数年後に振り返ったときに誇りに思える、より適切で理解しやすく保守しやすいコードを作成するのに役立ちます。
保守可能なコードの作成
ソフトウェアのバグの修正には費用がかかり、特にこれらのバグがすでにリリースされたソフトウェアに潜んで徐々に現れる場合には、時間の経過とともに、これらのバグのコストも増加します。バグを見つけたら、コードが解決しようとしている問題がまだ頭の中に明確に残っているうちに、バグを修正するのが最善です。そうしないと、他のタスクに移り、そのコードの特定の部分を忘れてしまい、しばらくしてからそのコードを再検討するときに次のことが必要になります:
問題の学習と理解に時間を費やす
解決すべき問題のコードを理解するのに時間をかける
また特に大規模なプロジェクトや企業の場合、問題は、バグを修正する人がコードを書いた人ではないことです(そして、バグを発見した人とそれを修正した人は同じ人ではありません)。したがって、以前に自分が書いたコードであっても、チームの他のメンバーが書いたコードであっても、コードを理解するのにかかる時間を短縮することが重要です。これは収益 (収益) と開発者の幸福に関係します。なぜなら、レガシー コードの保守に何時間も何日も費やすのではなく、新しくエキサイティングなものを構築する必要があるからです。
ソフトウェア開発に関するもう 1 つの事実は、コードを記述するよりもコードを読み取る方がはるかに時間がかかるということです。問題について集中して深く考えると、午後に座って大量のコードを書くことができることがあります。
コードは非常に迅速に機能しますが、アプリケーションが成熟するにつれて、他の多くのことが発生し、レビュー、変更、調整が必要になります。例:
バグが暴露される
新しい機能がアプリケーションに追加される
プログラムが新しい環境で動作する (例えば、新しいブラウザが市場に登場する)
コードの目的が変更される
コード完全に再起動するか、別のアーキテクチャに移植するか、別の言語を使用する必要があります
これらの変更により、少数の人が数時間かけて書いたコードが、最終的には読むのに数週間かかることになりました。このため、メンテナンス可能なコードを作成することがアプリケーションの成功にとって重要です。
保守可能なコードとは次のことを意味します:
読みやすい
一貫性がある
予測可能
同じ人によって書かれたように見える
文書化されている
グローバルの最小化
JavaScript 関数を通じてスコープを管理する。関数内で宣言された変数は関数内でのみ使用でき、関数の外では使用できません。一方、グローバル変数は、関数の外で宣言された変数、または宣言されずに単に使用される変数です。
すべての JavaScript 環境にはグローバル オブジェクトがあり、これを関数の外で使用するときにアクセスできます。作成したすべての変数は、このグローバル オブジェクトのプロパティになります。ブラウザでは、便宜上、グローバル オブジェクトには window と呼ばれる追加のプロパティがあり、これは (通常は) グローバル オブジェクト自体を指します。次のコード スニペットは、ブラウザ環境でグローバル変数がどのように作成され、アクセスされるかを示しています:
myglobal = "hello"; // 不推荐写法 console.log(myglobal); // "hello" console.log(window.myglobal); // "hello" console.log(window["myglobal"]); // "hello" console.log(this.myglobal); // "hello"
グローバル変数の問題
グローバル変数の問題は、JavaScript アプリケーションと Web ページのすべてのコードで共有されることです。 グローバル変数これらは同じグローバル名前空間に存在するため、プログラムの 2 つの異なる部分が同じ名前で異なる関数を持つグローバル変数を定義すると、名前の競合が避けられません。
Web ページには、ページの開発者によって記述されていないコードが含まれることもよくあります。例:
サードパーティの JavaScript ライブラリ
広告主のスクリプト コード
サードパーティのユーザー追跡および分析スクリプト コード
さまざまなタイプのウィジェット、ロゴ、ボタン
たとえば、サードパーティのスクリプトは result というグローバル変数を定義し、関数内でも result というグローバル変数を定義します。その結果、後の変数が前の変数を上書きし、サードパーティのスクリプトが突然げっぷをすることになります。
したがって、他のスクリプトと良い隣人でありたい場合は、グローバル変数の使用をできるだけ少なくすることが重要です。名前空間モードや自動関数の即時実行など、グローバル変数を削減するためのいくつかの戦略については、本書の後半で説明しますが、グローバル変数を削減するために最も重要なことは、常に var を使用して変数を宣言することです。
JavaScript の 2 つの特性により、無意識のうちにグローバル変数を作成するのは驚くほど簡単です。まず、変数は宣言しなくても使用できます。第 2 に、JavaScript には暗黙的なグローバルの概念があり、宣言していない変数はグローバル オブジェクト プロパティになります。以下のコードを参照してください:
function sum(x, y) { // 不推荐写法: 隐式全局变量 result = x + y; return result; }
此段代码中的result没有声明。代码照样运作正常,但在调用函数后你最后的结果就多一个全局命名空间,这可以是一个问题的根源。
经验法则是始终使用var声明变量,正如改进版的sum()函数所演示的:
function sum(x, y) { var result = x + y; return result; }
另一个创建隐式全局变量的反例就是使用任务链进行部分var声明。下面的片段中,a是本地变量但是b确实全局变量,这可能不是你希望发生的:
// 反例,勿使用 function foo() { var a = b = 0; // ... }
此现象发生的原因在于这个从右到左的赋值,首先,是赋值表达式b = 0,此情况下b是未声明的。这个表达式的返回值是0,然后这个0就分配给了通过var定义的这个局部变量a。换句话说,就好比你输入了:
var a = (b = 0);
如果你已经准备好声明变量,使用链分配是比较好的做法,不会产生任何意料之外的全局变量,如:
function foo() { var a, b; // ... a = b = 0; // 两个均局部变量 }
然而,另外一个避免全局变量的原因是可移植性。如果你想你的代码在不同的环境下(主机下)运行,使用全局变量如履薄冰,因为你会无意中覆盖你最初环境下不存在的主机对象(所以你原以为名称可以放心大胆地使用,实际上对于有些情况并不适用)。
忘记var的副作用(Side Effects When Forgetting var)
隐式全局变量和明确定义的全局变量间有些小的差异,就是通过delete操作符让变量未定义的能力。
通过var创建的全局变量(任何函数之外的程序中创建)是不能被删除的。
无var创建的隐式全局变量(无视是否在函数中创建)是能被删除的。
这表明,在技术上,隐式全局变量并不是真正的全局变量,但它们是全局对象的属性。属性是可以通过delete操作符删除的,而变量是不能的:
// 定义三个全局变量 var global_var = 1; global_novar = 2; // 反面教材 (function () { global_fromfunc = 3; // 反面教材 }()); // 试图删除 delete global_var; // false delete global_novar; // true delete global_fromfunc; // true // 测试该删除 typeof global_var; // "number" typeof global_novar; // "undefined" typeof global_fromfunc; // "undefined"
在ES5严格模式下,未声明的变量(如在前面的代码片段中的两个反面教材)工作时会抛出一个错误。
访问全局对象(Access to the Global Object)
在浏览器中,全局对象可以通过window属性在代码的任何位置访问(除非你做了些比较出格的事情,像是声明了一个名为window的局部变量)。但是在其他环境下,这个方便的属性可能被叫做其他什么东西(甚至在程序中不可用)。如果你需要在没有硬编码的window标识符下访问全局对象,你可以在任何层级的函数作用域中做如下操作:
var global = (function () { return this; }());
这种方法可以随时获得全局对象,因为其在函数中被当做函数调用了(不是通过new构造),this总是指向全局对象。实际上这个病不适用于ECMAScript 5严格模式,所以,在严格模式下时,你必须采取不同的形式。例如,你正在开发一个JavaScript库,你可以将你的代码包裹在一个即时函数中,然后从全局作用域中,传递一个引用指向this作为你即时函数的参数。
单var形式(Single var Pattern)
在函数顶部使用单var语句是比较有用的一种形式,其好处在于:
提供了一个单一的地方去寻找功能所需要的所有局部变量
防止变量在定义之前使用的逻辑错误
帮助你记住声明的全局变量,因此较少了全局变量//zxx:此处我自己是有点晕乎的…
少代码(类型啊传值啊单线完成)
单var形式长得就像下面这个样子:
function func() { var a = 1, b = 2, sum = a + b, myobject = {}, i, j; // function body... }
您可以使用一个var语句声明多个变量,并以逗号分隔。像这种初始化变量同时初始化值的做法是很好的。这样子可以防止逻辑错误(所有未初始化但声明的变量的初始值是undefined)和增加代码的可读性。在你看到代码后,你可以根据初始化的值知道这些变量大致的用途,例如是要当作对象呢还是当作整数来使。
你也可以在声明的时候做一些实际的工作,例如前面代码中的sum = a + b这个情况,另外一个例子就是当你使用DOM(文档对象模型)引用时,你可以使用单一的var把DOM引用一起指定为局部变量,就如下面代码所示的:
function updateElement() { var el = document.getElementById("result"), style = el.style; // 使用el和style干点其他什么事... }
预解析:var散布的问题(Hoisting: A Problem with Scattered vars)
JavaScript中,你可以在函数的任何位置声明多个var语句,并且它们就好像是在函数顶部声明一样发挥作用,这种行为称为 hoisting(悬置/置顶解析/预解析)。当你使用了一个变量,然后不久在函数中又重新声明的话,就可能产生逻辑错误。对于JavaScript,只 要你的变量是在同一个作用域中(同一函数),它都被当做是声明的,即使是它在var声明前使用的时候。看下面这个例子:
// 反例 myname = "global"; // 全局变量 function func() { alert(myname); // "undefined" var myname = "local"; alert(myname); // "local" } func();
在这个例子中,你可能会以为第一个alert弹出的是”global”,第二个弹出”loacl”。这种期许是可以理解的,因为在第一个alert的时候,myname未声明,此时函数肯定很自然而然地看全局变量myname,但是,实际上并不是这么工作的。第一个alert会弹 出”undefined”是因为myname被当做了函数的局部变量(尽管是之后声明的),所有的变量声明当被悬置到函数的顶部了。因此,为了避免这种混乱,最好是预先声明你想使用的全部变量。
上面的代码片段执行的行为可能就像下面这样:
myname = "global"; // global variable function func() { var myname; // 等同于 -> var myname = undefined; alert(myname); // "undefined" myname = "local"; alert(myname); // "local"} func();
为了完整,我们再提一提执行层面的稍微复杂点的东西。代码处理分两个阶段,第一阶段是变量,函数声明,以及正常格式的参数创建,这是一个解析和进入上下文 的阶段。第二个阶段是代码执行,函数表达式和不合格的标识符(为声明的变量)被创建。但是,出于实用的目的,我们就采用了”hoisting”这个概念, 这种ECMAScript标准中并未定义,通常用来描述行为。
for循环(for Loops)
在for循环中,你可以循环取得数组或是数组类似对象的值,譬如arguments和HTMLCollection对象。通常的循环形式如下:
// 次佳的循环 for (var i = 0; i < myarray.length; i++) { // 使用myarray[i]做点什么 }
这种形式的循环的不足在于每次循环的时候数组的长度都要去获取下。这回降低你的代码,尤其当myarray不是数组,而是一个HTMLCollection对象的时候。
HTMLCollections指的是DOM方法返回的对象,例如:
document.getElementsByName() document.getElementsByClassName() document.getElementsByTagName()
还有其他一些HTMLCollections,这些是在DOM标准之前引进并且现在还在使用的。有:
document.images: 页面上所有的图片元素 document.links : 所有a标签元素 document.forms : 所有表单 document.forms[0].elements : 页面上第一个表单中的所有域
集合的麻烦在于它们实时查询基本文档(HTML页面)。这意味着每次你访问任何集合的长度,你要实时查询DOM,而DOM操作一般都是比较昂贵的。
这就是为什么当你循环获取值时,缓存数组(或集合)的长度是比较好的形式,正如下面代码显示的:
for (var i = 0, max = myarray.length; i < max; i++) { // 使用myarray[i]做点什么 }
这样,在这个循环过程中,你只检索了一次长度值。
在所有浏览器下,循环获取内容时缓存HTMLCollections的长度是更快的,2倍(Safari3)到190倍(IE7)之间。//zxx:此数据貌似很老,仅供参考。
注意到,当你明确想要修改循环中的集合的时候(例如,添加更多的DOM元素),你可能更喜欢长度更新而不是常量。
伴随着单var形式,你可以把变量从循环中提出来,就像下面这样:
function looper() { var i = 0, max, myarray = []; // ... for (i = 0, max = myarray.length; i < max; i++) { // 使用myarray[i]做点什么 } }
这种形式具有一致性的好处,因为你坚持了单一var形式。不足在于当重构代码的时候,复制和粘贴整个循环有点困难。例如,你从一个函数复制了一个循环到另一个函数,你不得不去确定你能够把i和max引入新的函数(如果在这里没有用的话,很有可能你要从原函数中把它们删掉)。
最后一个需要对循环进行调整的是使用下面表达式之一来替换i++。
i = i + 1 i += 1
JSLint提示您这样做,原因是++和–-促进了“过分棘手(excessive trickiness)”。//zxx:这里比较难翻译,我想本意应该是让代码变得更加的棘手。如果你直接无视它,JSLint的plusplus选项会是false(默认是default)。
还有两种变化的形式,其又有了些微改进,因为:
少了一个变量(无max)
向下数到0,通常更快,因为和0做比较要比和数组长度或是其他不是0的东西作比较更有效率
//第一种变化的形式: var i, myarray = []; for (i = myarray.length; i–-;) { // 使用myarray[i]做点什么 } //第二种使用while循环: var myarray = [], i = myarray.length; while (i–-) { // 使用myarray[i]做点什么 }
这些小的改进只体现在性能上,此外JSLint会对使用i–-加以抱怨。
for-in循环(for-in Loops)
for-in循环应该用在非数组对象的遍历上,使用for-in进行循环也被称为“枚举”。
从技术上将,你可以使用for-in循环数组(因为JavaScript中数组也是对象),但这是不推荐的。因为如果数组对象已被自定义的功能增强,就可能发生逻辑错误。另外,在for-in中,属性列表的顺序(序列)是不能保证的。所以最好数组使用正常的for循环,对象使用for-in循环。
有个很重要的hasOwnProperty()方法,当遍历对象属性的时候可以过滤掉从原型链上下来的属性。
思考下面一段代码:
// 对象 var man = { hands: 2, legs: 2, heads: 1 }; // 在代码的某个地方 // 一个方法添加给了所有对象 if (typeof Object.prototype.clone === "undefined") { Object.prototype.clone = function () {}; }
在这个例子中,我们有一个使用对象字面量定义的名叫man的对象。在man定义完成后的某个地方,在对象原型上增加了一个很有用的名叫clone()的方法。此原型链是实时的,这就意味着所有的对象自动可以访问新的方法。为了避免枚举man的时候出现clone()方法,你需要应用hasOwnProperty()方法过滤原型属性。如果不做过滤,会导致clone()函数显示出来,在大多数情况下这是不希望出现的。
// 1. // for-in 循环 for (var i in man) { if (man.hasOwnProperty(i)) { // 过滤 console.log(i, ":", man[i]); } } /* 控制台显示结果 hands : 2 legs : 2 heads : 1 */ // 2. // 反面例子: // for-in loop without checking hasOwnProperty() for (var i in man) { console.log(i, ":", man[i]); } /* 控制台显示结果 hands : 2 legs : 2 heads : 1 clone: function() */
另外一种使用hasOwnProperty()的形式是取消Object.prototype上的方法。像是:
for (var i in man) { if (Object.prototype.hasOwnProperty.call(man, i)) { // 过滤 console.log(i, ":", man[i]); } }
其好处在于在man对象重新定义hasOwnProperty情况下避免命名冲突。也避免了长属性查找对象的所有方法,你可以使用局部变量“缓存”它。
var i, hasOwn = Object.prototype.hasOwnProperty; for (i in man) { if (hasOwn.call(man, i)) { // 过滤 console.log(i, ":", man[i]); } }
严格来说,不使用hasOwnProperty()并不是一个错误。根据任务以及你对代码的自信程度,你可以跳过它以提高些许的循环速度。但是当你对当前对象内容(和其原型链)不确定的时候,添加hasOwnProperty()更加保险些。
格式化的变化(通不过JSLint)会直接忽略掉花括号,把if语句放到同一行上。其优点在于循环语句读起来就像一个完整的想法(每个元素都有一个自己的属性”X”,使用”X”干点什么):
// 警告: 通不过JSLint检测 var i, hasOwn = Object.prototype.hasOwnProperty; for (i in man) if (hasOwn.call(man, i)) { // 过滤 console.log(i, ":", man[i]); }
(不)扩展内置原型((Not) Augmenting Built-in Prototypes)
扩增构造函数的prototype属性是个很强大的增加功能的方法,但有时候它太强大了。
增加内置的构造函数原型(如Object(),Array(), 或Function())挺诱人的,但是这严重降低了可维护性,因为它让你的代码变得难以预测。使用你代码的其他开发人员很可能更期望使用内置的 JavaScript方法来持续不断地工作,而不是你另加的方法。
另外,属性添加到原型中,可能会导致不使用hasOwnProperty属性时在循环中显示出来,这会造成混乱。
因此,不增加内置原型是最好的。你可以指定一个规则,仅当下面的条件均满足时例外:
可以预期将来的ECMAScript版本或是JavaScript实现将一直将此功能当作内置方法来实现。例如,你可以添加ECMAScript
5中描述的方法,一直到各个浏览器都迎头赶上。这种情况下,你只是提前定义了有用的方法。
如果您检查您的自定义属性或方法已不存在——也许已经在代码的其他地方实现或已经是你支持的浏览器JavaScript引擎部分。
你清楚地文档记录并和团队交流了变化。
如果这三个条件得到满足,你可以给原型进行自定义的添加,形式如下:
if (typeof Object.protoype.myMethod !== "function") { Object.protoype.myMethod = function () { // 实现... }; }
switch模式(switch Pattern)
你可以通过类似下面形式的switch语句增强可读性和健壮性:
var inspect_me = 0, result = ''; switch (inspect_me) { case 0: result = "zero"; break; case 1: result = "one"; break; default: result = "unknown"; }
这个简单的例子中所遵循的风格约定如下:
每个case和switch对齐(花括号缩进规则除外)
每个case中代码缩进
每个case以break清除结束
避免贯穿(故意忽略break)。如果你非常确信贯穿是最好的方法,务必记录此情况,因为对于有些阅读人而言,它们可能看起来是错误的。
以default结束switch:确保总有健全的结果,即使无情况匹配。
避免隐式类型转换(Avoiding Implied Typecasting )
JavaScript的变量在比较的时候会隐式类型转换。这就是为什么一些诸如:false == 0或“” == 0返回的结果是true。为避免引起混乱的隐含类型转换,在你比较值和表达式类型的时候始终使用===和!==操作符。
var zero = 0; if (zero === false) { // 不执行,因为zero为0, 而不是false } // 反面示例 if (zero == false) { // 执行了... }
还有另外一种思想观点认为==就足够了===是多余的。例如,当你使用typeof你就知道它会返回一个字符串,所以没有使用严格相等的理由。然而,JSLint要求严格相等,它使代码看上去更有一致性,可以降低代码阅读时的精力消耗。(“==是故意的还是一个疏漏?”)
避免(Avoiding)eval()
如果你现在的代码中使用了eval(),记住该咒语“eval()是魔鬼”。此方法接受任意的字符串,并当作JavaScript代码来处理。当有 问题的代码是事先知道的(不是运行时确定的),没有理由使用eval()。如果代码是在运行时动态生成,有一个更好的方式不使用eval而达到同样的目标。例如,用方括号表示法来访问动态属性会更好更简单:
// 反面示例 var property = "name"; alert(eval("obj." + property)); // 更好的 var property = "name"; alert(obj[property]);
使用eval()也带来了安全隐患,因为被执行的代码(例如从网络来)可能已被篡改。这是个很常见的反面教材,当处理Ajax请求得到的JSON 相应的时候。在这些情况下,最好使用JavaScript内置方法来解析JSON相应,以确保安全和有效。若浏览器不支持JSON.parse(),你可 以使用来自JSON.org的库。
同样重要的是要记住,给setInterval(),setTimeout()和Function()构造函数传递字符串,大部分情况下,与使用eval()是类似的,因此要避免。在幕后,JavaScript仍需要评估和执行你给程序传递的字符串:
// 反面示例 setTimeout("myFunc()", 1000); setTimeout("myFunc(1, 2, 3)", 1000); // 更好的 setTimeout(myFunc, 1000); setTimeout(function () { myFunc(1, 2, 3); }, 1000);
使用新的Function()构造就类似于eval(),应小心接近。这可能是一个强大的构造,但往往被误用。如果你绝对必须使用eval(),你 可以考虑使用new Function()代替。有一个小的潜在好处,因为在新Function()中作代码评估是在局部函数作用域中运行,所以代码中任何被评估的通过var 定义的变量都不会自动变成全局变量。另一种方法来阻止自动全局变量是封装eval()调用到一个即时函数中。
考虑下面这个例子,这里仅un作为全局变量污染了命名空间。
console.log(typeof un); // "undefined" console.log(typeof deux); // "undefined" console.log(typeof trois); // "undefined" var jsstring = "var un = 1; console.log(un);"; eval(jsstring); // logs "1" jsstring = "var deux = 2; console.log(deux);"; new Function(jsstring)(); // logs "2" jsstring = "var trois = 3; console.log(trois);"; (function () { eval(jsstring); }()); // logs "3" console.log(typeof un); // number console.log(typeof deux); // "undefined" console.log(typeof trois); // "undefined"
另一间eval()和Function构造不同的是eval()可以干扰作用域链,而Function()更安分守己些。不管你在哪里执行Function(),它只看到全局作用域。所以其能很好的避免本地变量污染。在下面这个例子中,eval()可以访问和修改它外部作用域中的变量,这是 Function做不来的(注意到使用Function和new Function是相同的)。
(function () { var local = 1; eval("local = 3; console.log(local)"); // logs "3" console.log(local); // logs "3" }()); (function () { var local = 1; Function("console.log(typeof local);")(); // logs undefined }()); parseInt()下的数值转换(Number Conversions with parseInt())
使用parseInt()你可以从字符串中获取数值,该方法接受另一个基数参数,这经常省略,但不应该。当字符串以”0″开头的时候就有可能会出问 题,例如,部分时间进入表单域,在ECMAScript 3中,开头为”0″的字符串被当做8进制处理了,但这已在ECMAScript 5中改变了。为了避免矛盾和意外的结果,总是指定基数参数。
var month = "06", year = "09"; month = parseInt(month, 10); year = parseInt(year, 10);
此例中,如果你忽略了基数参数,如parseInt(year),返回的值将是0,因为“09”被当做8进制(好比执行parseInt( year, 8 )),而09在8进制中不是个有效数字。
替换方法是将字符串转换成数字,包括:
+"08" // 结果是 8 Number("08") // 8
这些通常快于parseInt(),因为parseInt()方法,顾名思意,不是简单地解析与转换。但是,如果你想输入例如“08 hello”,parseInt()将返回数字,而其它以NaN告终。
编码规范(Coding Conventions)
建立和遵循编码规范是很重要的,这让你的代码保持一致性,可预测,更易于阅读和理解。一个新的开发者加入这个团队可以通读规范,理解其它团队成员书写的代码,更快上手干活。
许多激烈的争论发生会议上或是邮件列表上,问题往往针对某些代码规范的特定方面(例如代码缩进,是Tab制表符键还是space空格键)。如果你是 你组织中建议采用规范的,准备好面对各种反对的或是听起来不同但很强烈的观点。要记住,建立和坚定不移地遵循规范要比纠结于规范的细节重要的多。
缩进(Indentation)
代码没有缩进基本上就不能读了。唯一糟糕的事情就是不一致的缩进,因为它看上去像是遵循了规范,但是可能一路上伴随着混乱和惊奇。重要的是规范地使用缩进。
一些开发人员更喜欢用tab制表符缩进,因为任何人都可以调整他们的编辑器以自己喜欢的空格数来显示Tab。有些人喜欢空格——通常四个,这都无所谓,只要团队每个人都遵循同一个规范就好了。这本书,例如,使用四个空格缩进,这也是JSLint中默认的缩进。
什么应该缩进呢?规则很简单——花括号里面的东西。这就意味着函数体,循环 (do, while, for, for-in),if,switch,以及对象字面量中的对象属性。下面的代码就是使用缩进的示例:
function outer(a, b) { var c = 1, d = 2, inner; if (a > b) { inner = function () { return { r: c - d }; }; } else { inner = function () { return { r: c + d }; }; } return inner; }
花括号{}(Curly Braces)
花括号(亦称大括号,下同)应总被使用,即使在它们为可选的时候。技术上将,在in或是for中如果语句仅一条,花括号是不需要的,但是你还是应该总是使用它们,这会让代码更有持续性和易于更新。
想象下你有一个只有一条语句的for循环,你可以忽略花括号,而没有解析的错误。
// 糟糕的实例 for (var i = 0; i < 10; i += 1) alert(i);
但是,如果,后来,主体循环部分又增加了行代码?
// 糟糕的实例 for (var i = 0; i < 10; i += 1) alert(i); alert(i + " is " + (i % 2 ? "odd" : "even"));
第二个alert已经在循环之外,缩进可能欺骗了你。为了长远打算,最好总是使用花括号,即时值一行代码:
// 好的实例 for (var i = 0; i < 10; i += 1) { alert(i); }
if条件类似:
// 坏 if (true) alert(1); else alert(2); // 好 if (true) { alert(1); } else { alert(2); }
左花括号的位置(Opening Brace Location)
开发人员对于左大括号的位置有着不同的偏好——在同一行或是下一行。
if (true) { alert("It's TRUE!"); } //或 if (true) { alert("It's TRUE!"); }
这个实例中,仁者见仁智者见智,但也有个案,括号位置不同会有不同的行为表现。这是因为分号插入机制(semicolon insertion mechanism)——JavaScript是不挑剔的,当你选择不使用分号结束一行代码时JavaScript会自己帮你补上。这种行为可能会导致麻 烦,如当你返回对象字面量,而左括号却在下一行的时候:
// 警告: 意外的返回值 function func() { return // 下面代码不执行 { name : "Batman" } }
如果你希望函数返回一个含有name属性的对象,你会惊讶。由于隐含分号,函数返回undefined。前面的代码等价于:
// 警告: 意外的返回值 function func() { return undefined; // 下面代码不执行 { name : "Batman" } }
总之,总是使用花括号,并始终把在与之前的语句放在同一行:
function func() { return { name : "Batman" }; }
关于分号注:就像使用花括号,你应该总是使用分号,即使他们可由JavaScript解析器隐式创建。这不仅促进更科学和更严格的代码,而且有助于解决存有疑惑的地方,就如前面的例子显示。
空格(White Space)
空格的使用同样有助于改善代码的可读性和一致性。在写英文句子的时候,在逗号和句号后面会使用间隔。在JavaScript中,你可以按照同样的逻辑在列表模样表达式(相当于逗号)和结束语句(相对于完成了“想法”)后面添加间隔。
适合使用空格的地方包括:
for循环分号分开后的的部分:如for (var i = 0; i < 10; i += 1) {...}
for循环中初始化的多变量(i和max):for (var i = 0, max = 10; i < max; i += 1) {...}
分隔数组项的逗号的后面:var a = [1, 2, 3];
对象属性逗号的后面以及分隔属性名和属性值的冒号的后面:var o = {a: 1, b: 2};
限定函数参数:myFunc(a, b, c)
函数声明的花括号的前面:function myFunc() {}
匿名函数表达式function的后面:var myFunc = function () {};
使用空格分开所有的操作符和操作对象是另一个不错的使用,这意味着在+, -, *, =, <, >, <=, >=, ===, !==, &&, ||, +=等前后都需要空格。
// 宽松一致的间距 // 使代码更易读 // 使得更加“透气” var d = 0, a = b + 1; if (a && b && c) { d = a % c; a += d; } // 反面例子 // 缺失或间距不一 // 使代码变得疑惑 var d = 0, a = b + 1; if (a&&b&&c) { d=a % c; a+= d; }
最后需要注意的一个空格——花括号间距。最好使用空格:
函数、if-else语句、循环、对象字面量的左花括号的前面({)
else或while之间的右花括号(})
空格使用的一点不足就是增加了文件的大小,但是压缩无此问题。
有一个经常被忽略的代码可读性方面是垂直空格的使用。你可以使用空行来分隔代码单元,就像是文学作品中使用段落分隔一样。
命名规范(Naming Conventions)
另一种方法让你的代码更具可预测性和可维护性是采用命名规范。这就意味着你需要用同一种形式给你的变量和函数命名。
下面是建议的一些命名规范,你可以原样采用,也可以根据自己的喜好作调整。同样,遵循规范要比规范是什么更重要。
以大写字母写构造函数(Capitalizing Constructors)
JavaScript并没有类,但有new调用的构造函数:
var adam = new Person();
因为构造函数仍仅仅是函数,仅看函数名就可以帮助告诉你这应该是一个构造函数还是一个正常的函数。
命名构造函数时首字母大写具有暗示作用,使用小写命名的函数和方法不应该使用new调用:
function MyConstructor() {...} function myFunction() {...}
分隔单词(Separating Words)
当你的变量或是函数名有多个单词的时候,最好单词的分离遵循统一的规范,有一个常见的做法被称作“驼峰(Camel)命名法”,就是单词小写,每个单词的首字母大写。
对于构造函数,可以使用大驼峰式命名法(upper camel case),如MyConstructor()。对于函数和方法名称,你可以使用小驼峰式命名法(lower camel case),像是myFunction(),calculateArea()和getFirstName()。
要是变量不是函数呢?开发者通常使用小驼峰式命名法,但还有另外一种做法就是所有单词小写以下划线连接:例如,first_name,favorite_bands, 和old_company_name,这种标记法帮你直观地区分函数和其他标识——原型和对象。
ECMAScript的属性和方法均使用Camel标记法,尽管多字的属性名称是罕见的(正则表达式对象的lastIndex和ignoreCase属性)。
其它命名形式(Other Naming Patterns)
有时,开发人员使用命名规范来弥补或替代语言特性。
例如,JavaScript中没有定义常量的方法(尽管有些内置的像Number,MAX_VALUE),所以开发者都采用全部单词大写的规范来命名这个程序生命周期中都不会改变的变量,如:
// 珍贵常数,只可远观 var PI = 3.14, MAX_WIDTH = 800;
还有另外一个完全大写的惯例:全局变量名字全部大写。全部大写命名全局变量可以加强减小全局变量数量的实践,同时让它们易于区分。
另外一种使用规范来模拟功能的是私有成员。虽然可以在JavaScript中实现真正的私有,但是开发者发现仅仅使用一个下划线前缀来表示一个私有属性或方法会更容易些。考虑下面的例子:
var person = { getName: function () { return this._getFirst() + ' ' + this._getLast(); }, _getFirst: function () { // ... }, _getLast: function () { // ... } };
在此例中,getName()就表示公共方法,部分稳定的API。而_getFirst()和_getLast()则表明了私有。它们仍然是正常的公共方法,但是使用下划线前缀来警告person对象的使用者这些方法在下一个版本中时不能保证工作的,是不能直接使用的。注意,JSLint有些不鸟下划线前缀,除非你设置了noman选项为:false。
下面是一些常见的_private规范:
使用尾下划线表示私有,如name_和getElements_()
_protected (保護された) 属性を表すにはアンダースコア接頭辞を使用し、__private (プライベート) 属性を表すには 2 つのアンダースコア接頭辞を使用します。
Firefox の一部の組み込み変数属性は、言語の技術的な部分に属しません。先頭に 2 つのアンダースコアを使用します。末尾の 2 つのアンダースコアは、__proto__ や __parent__ などを示します。
コメント (コメントを書く)
たとえ他の人が同じようにコードに触れないとしても、コードをコメントする必要があります。通常、問題を深く研究すると、コードが何に使用されているかが明確にわかりますが、1 週間後にその問題に戻ってみると、それがどのように機能するかを理解するために多くの脳細胞を費やしたはずです。
明らかに、コメントは、変数ごとに分けたり、行を分けたりといった極端なことはできません。ただし、通常はすべての関数、その引数と戻り値、または珍しいテクニックやメソッドを文書化する必要があります。コメントは、今後コードを読む人に多くの手がかりを与える可能性があることに注意してください。読者は、コードを理解するために (あまり読みすぎなくても) コメントと関数の属性名だけを必要とします。たとえば、特定のタスクを実行するプログラムが 5 行または 6 行ある場合、その行の目的とその行が存在する理由を説明すれば、読者はこの詳細をスキップできます。コードに対するコメントの比率について厳密な規則はなく、コードの一部 (正規表現など) にはコードよりも多くのコメントが含まれる場合があります。
最も重要な習慣ですが、従うのが最も難しい習慣は、コメントを常に最新の状態に保つことです。古いコメントは、まったくコメントしないよりも誤解を招くためです。
著者について
Stoyan Stefanov は、Yahoo! Web 開発者、著者、寄稿者、および複数の O'Reilly 書籍の技術評論家です。彼はカンファレンスやブログ www.phpied.com で Web 開発のトピックについて頻繁に講演しています。 Stoyan は、smush.it 画像最適化ツールの作成者でもあり、YUI の貢献者でもあり、Yahoo のパフォーマンス最適化ツール YSlow 2.0 のアーキテクトでもあります。