英文原文:The Essentials of Writing High Quality JavaScript
原文作者:Stoyan Stefanov
譯者:張鑫旭
原文地址:http://www.zhangxinxu.com/wordpress/?p=1173
才華橫溢的Stoyan Stefanov,在他寫的由O’Reilly出版的新書《JavaScript Patterns》(JavaScript模式)中没龙,我想要是為我們的讀者貢獻(xiàn)其摘要刃永,那會是件很美妙的事情俐填。具體一點就是編寫高質(zhì)量JavaScript的一些要素,例如避免全局變量挽拔,使用單變量聲明吼拥,在循環(huán)中預(yù)緩存length(長度),遵循代碼閱讀,以及更多诲锹。
此摘要也包括一些與代碼不太相關(guān)的習(xí)慣繁仁,但對整體代碼的創(chuàng)建息息相關(guān),包括撰寫API文檔归园、執(zhí)行同行評審以及運行JSLint级遭。這些習(xí)慣和最佳做法可以幫助你寫出更好的冰垄,更易于理解和維護(hù)的代碼搀崭,這些代碼在幾個月或是幾年之后再回過頭看看也是會覺得很自豪的敞咧。
書寫可維護(hù)的代碼(Writing Maintainable Code )
軟件bug的修復(fù)是昂貴的,并且隨著時間的推移偶翅,這些bug的成本也會增加,尤其當(dāng)這些bug潛伏并慢慢出現(xiàn)在已經(jīng)發(fā)布的軟件中時碉渡。當(dāng)你發(fā)現(xiàn)bug的時候就立即修復(fù)它是最好的聚谁,此時你代碼要解決的問題,在你腦中還是很清晰的滞诺。否則形导,你轉(zhuǎn)移到其他任務(wù),忘了那個特定的代碼习霹,一段時間后再去查看這些代碼就需要:
- 花時間學(xué)習(xí)和理解這個問題
- 花時間是了解應(yīng)該解決的問題代碼
還有問題朵耕,特別對于大的項目或是公司,修復(fù)bug的這位伙計不是寫代碼的那個人(且發(fā)現(xiàn)bug和修復(fù)bug的不是同一個人)淋叶。因此阎曹,必須降低理解代碼花費的時間,無論是一段時間前你自己寫的代碼煞檩,還是團(tuán)隊中的其他成員寫的代碼处嫌。這關(guān)系到底線(營業(yè)收入)和開發(fā)人員的幸福,因為我們更應(yīng)該去開發(fā)新的激動人心的事物斟湃,而不是花幾小時幾天的時間去維護(hù)遺留代碼熏迹。
另一個相關(guān)軟件開發(fā)生命的事實是,讀代碼花費的時間要比寫來得多凝赛。有時候注暗,當(dāng)你專注并深入思考某個問題的時候,你可以坐下來墓猎,一個下午寫大量的代碼捆昏。
你的代碼很能很快就工作了。但是毙沾,隨著應(yīng)用的成熟屡立,還會有很多其他的事情發(fā)生,這就要求你得進(jìn)行審查,修改和調(diào)整膨俐。例如:
- bug是暴露的
- 新功能被添加到應(yīng)用程序
- 程序在新的環(huán)境下工作(例如勇皇,市場上出現(xiàn)新想瀏覽器)
- 代碼改變用途
- 代碼得完全從頭重新移植到另一個架構(gòu)上,或者甚至使用另一種語言
由于這些變化焚刺,很少人力數(shù)小時寫的代碼敛摘,最終演變成花數(shù)周來閱讀這些代碼。這就是為什么創(chuàng)建可維護(hù)的代碼乳愉,對應(yīng)用程序的成功至關(guān)重要兄淫。
可維護(hù)的代碼意味著:
- 可讀的
- 一致的
- 可預(yù)測的
- 看上去就像是同一個人寫的
- 已記錄
最小全局變量(Minimizing Globals)
JavaScript通過函數(shù)管理作用域。在函數(shù)內(nèi)部聲明的變量蔓姚,只在這個函數(shù)內(nèi)部捕虽,函數(shù)外面不可用。另一方面坡脐,全局變量就是在任何函數(shù)外面聲明的泄私,或是未聲明直接簡單使用的。
每個JavaScript環(huán)境有一個全局對象备闲,當(dāng)你在任意的函數(shù)外面使用this
的時候可以訪問到晌端。你創(chuàng)建的每一個全部變量都成了這個全局對象的屬性。在瀏覽器中恬砂,方便起見咧纠,該全局對象有個附加屬性叫做window,此window(通常)指向該全局對象本身泻骤。下面的代碼片段顯示了如何在瀏覽器環(huán)境中創(chuàng)建和訪問的全局變量:
myglobal = "hello"; // 不推薦寫法
console.log(myglobal); // "hello"
console.log(window.myglobal); // "hello"
console.log(window["myglobal"]); // "hello"
console.log(this.myglobal); // "hello"
全局變量的問題
全局變量的問題在于漆羔,你的JavaScript應(yīng)用程序和web頁面上的所有代碼都共享了這些全局變量,他們住在同一個全局命名空間狱掂。所以當(dāng)程序的兩個不同部分定義同名钧椰,但不同作用的全局變量的時候,命名沖突在所難免符欠。
web頁面包含不是該頁面開發(fā)者所寫的代碼嫡霞,也是比較常見的,例如:
- 第三方的JavaScript庫
- 廣告方的腳本代碼
- 第三方用戶跟蹤和分析腳本代碼
- 不同類型的小組件希柿,標(biāo)志和按鈕
比方說诊沪,該第三方腳本定義了一個全局變量,叫做result曾撤;接著端姚,在你的函數(shù)中也定義一個名為result的全局變量。其結(jié)果就是后面的變量覆蓋前面的挤悉,第三方腳本就一下子嗝屁啦渐裸!
因此,要想和其他腳本成為好鄰居的話,盡可能少的使用全局變量是很重要的昏鹃。在書中后面提到的一些減少全局變量的策略尚氛,例如命名空間模式或是函數(shù)立即自動執(zhí)行,但是要想讓全局變量少洞渤,最重要的還是始終使用var
來聲明變量阅嘶。
由于JavaScript的兩個特征,不自覺地創(chuàng)建出全局變量是出乎意料的容易载迄。首先讯柔,你可以甚至不需要聲明就可以使用變量;第二护昧,JavaScript有隱含的全局概念魂迄,意味著你不聲明的任何變量都會成為一個全局對象屬性。參考下面的代碼:
function sum(x, y) {
// 不推薦寫法: 隱式全局變量
result = x + y;
return result;
}
此段代碼中的result
沒有聲明惋耙。代碼照樣運作正常捣炬,但在調(diào)用函數(shù)后你最后的結(jié)果就多一個全局命名空間,這可以是一個問題的根源怠晴。
經(jīng)驗法則是始終使用var
聲明變量,正如改進(jìn)版的sum()
函數(shù)所演示的:
function sum(x, y) {
var result = x + y;
return result;
}
另一個創(chuàng)建隱式全局變量的反例就是使用任務(wù)鏈進(jìn)行部分var
聲明浴捆。下面的片段中蒜田,a
是本地變量,但是b
確實全局變量选泻,這可能不是你希望發(fā)生的:
// 反例冲粤,勿使用
function foo() {
var a = b = 0;
// ...
}
此現(xiàn)象發(fā)生的原因在于這個從右到左的賦值,首先页眯,是賦值表達(dá)式b = 0
梯捕,此情況下b
是未聲明的。這個表達(dá)式的返回值是0
窝撵,然后這個0
就分配給了通過var
定義的這個局部變量a
傀顾。換句話說,就好比你輸入了:
var a = (b = 0);
如果你已經(jīng)準(zhǔn)備好聲明變量碌奉,使用鏈分配是比較好的做法短曾,不會產(chǎn)生任何意料之外的全局變量,如:
function foo() {
var a, b;
// ... a = b = 0; // 兩個均局部變量
}
然而赐劣,另外一個避免全局變量的原因是可移植性嫉拐。如果想讓你的代碼在不同的環(huán)境下(主機(jī)下)運行,使用全局變量如履薄冰魁兼,因為你會無意中覆蓋你最初環(huán)境下不存在的主機(jī)對象(所以婉徘,你原以為名稱可以放心大膽地使用,實際上對于有些情況并不適用)。
忘記var的副作用(Side Effects When Forgetting var)
隱式全局變量和明確定義的全局變量間有些小的差異盖呼,就是通過delete
操作符讓變量未定義的能力儒鹿。
- 通過
var
創(chuàng)建的全局變量(任何函數(shù)之外的程序中創(chuàng)建)是不能被刪除的。 - 無
var
創(chuàng)建的隱式全局變量(無視是否在函數(shù)中創(chuàng)建)是能被刪除的塌计。
這表明挺身,在技術(shù)上,隱式全局變量并不是真正的全局變量锌仅,但它們是全局對象的屬性章钾。屬性是可以通過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嚴(yán)格模式下热芹,未聲明的變量(如在前面的代碼片段中的兩個反面教材)工作時會拋出一個錯誤贱傀。
訪問全局對象(Access to the Global Object)
在瀏覽器中,全局對象可以通過window
屬性在代碼的任何位置訪問(除非你做了些比較出格的事情伊脓,像是聲明了一個名為window
的局部變量)府寒。但是在其他環(huán)境下,這個方便的屬性可能被叫做其他什么東西(甚至在程序中不可用)报腔。如果你需要在沒有硬編碼的window
標(biāo)識符下訪問全局對象株搔,你可以在任何層級的函數(shù)作用域中做如下操作:
var global = (function () {
return this;
}());
這種方法可以隨時獲得全局對象,因為其在函數(shù)中被當(dāng)做函數(shù)調(diào)用了(不是通過new
構(gòu)造)纯蛾,this
總是指向全局對象纤房。實際上這個病不適用于ECMAScript 5嚴(yán)格模式,所以翻诉,在嚴(yán)格模式下時炮姨,你必須采取不同的形式。例如碰煌,你正在開發(fā)一個JavaScript庫舒岸,你可以將你的代碼包裹在一個即時函數(shù)中,然后從全局作用域中芦圾,傳遞一個引用指向this作為你即時函數(shù)的參數(shù)蛾派。
單var形式(Single var Pattern)
在函數(shù)頂部使用單var
語句是比較有用的一種形式,其好處在于:
- 提供了一個單一的地方去尋找功能所需要的所有局部變量
- 防止變量在定義之前使用的邏輯錯誤
- 幫助你記住聲明的全局變量个少,因此較少了全局變量
//zxx:此處我自己是有點暈乎的…
- 少代碼(類型啊傳值啊單線完成)
單var
形式長得就像下面這個樣子:
function func() {
var a = 1,
b = 2,
sum = a + b,
myobject = {},
i,
j;
// function body...
}
您可以使用一個var
語句聲明多個變量碍脏,并以逗號分隔。像這種初始化變量同時初始化值的做法是很好的稍算。這樣子可以防止邏輯錯誤(所有未初始化但聲明的變量的初始值是undefined
)和增加代碼的可讀性典尾。在你看到代碼后,你可以根據(jù)初始化的值糊探,知道這些變量大致的用途钾埂。例如河闰,是要當(dāng)作對象呢,還是當(dāng)作整數(shù)來使褥紫。
你也可以在聲明的時候做一些實際的工作姜性。例如,前面代碼中的sum = a + b
這個情況髓考。另外一個例子就是當(dāng)你使用DOM(文檔對象模型)引用時部念,你可以使用單一的var
把DOM引用一起指定為局部變量,就如下面代碼所示的:
function updateElement() {
var el = document.getElementById("result"),
style = el.style;
// 使用el和style干點其他什么事...
}
預(yù)解析:var散布的問題(Hoisting: A Problem with Scattered vars)
JavaScript中氨菇,你可以在函數(shù)的任何位置聲明多個var語句儡炼,并且它們就好像是在函數(shù)頂部聲明一樣發(fā)揮作用,這種行為稱為hoisting(懸置/置頂解析/預(yù)解析)查蓉。當(dāng)你使用了一個變量乌询,然后不久在函數(shù)中又重新聲明的話,就可能產(chǎn)生邏輯錯誤豌研。對于JavaScript妹田,只要你的變量是在同一個作用域中(同一函數(shù)),它都被當(dāng)做是聲明的鹃共,即使是它在var聲明前使用的時候鬼佣。看下面這個例子:
// 反例
myname = "global"; // 全局變量
function func() {
alert(myname); // "undefined"
var myname = "local";
alert(myname); // "local"
}
func();
在這個例子中霜浴,你可能會以為第一個alert彈出的是”global”晶衷,第二個彈出”loacl”。這種期許是可以理解的坷随,因為在第一個alert的時候房铭,myname
未聲明驻龟,此時函數(shù)肯定很自然而然地看全局變量myname
温眉。但是,實際上并不是這么工作的翁狐。第一個alert會彈出”undefined”类溢,是因為myname
被當(dāng)做了函數(shù)的局部變量(盡管是之后聲明的),所有的變量聲明當(dāng)被懸置到函數(shù)的頂部了露懒。因此闯冷,為了避免這種混亂,最好是預(yù)先聲明你想使用的全部變量懈词。
上面的代碼片段執(zhí)行的行為蛇耀,可能就像下面這樣:
myname = "global"; // global variable
function func() {
var myname; // 等同于 -> var myname = undefined;
alert(myname); // "undefined"
myname = "local";
alert(myname); // "local"}
func();
關(guān)于JavaScript的置頂解析,我上周專門翻譯了篇文章坎弯,您有興趣可以看看:“翻譯 – 解釋JavaScript的置頂解析”纺涤。
為了完整译暂,我們再提一提執(zhí)行層面稍微復(fù)雜點的東西。代碼處理分兩個階段:第一階段是變量撩炊,函數(shù)聲明外永,以及正常格式的參數(shù)創(chuàng)建,這是一個解析和進(jìn)入上下文的階段拧咳。第二個階段是代碼執(zhí)行伯顶,函數(shù)表達(dá)式和不合格的標(biāo)識符(為聲明的變量)被創(chuàng)建。但是骆膝,出于實用的目的祭衩,我們就采用了”hoisting”這個概念,這種ECMAScript標(biāo)準(zhǔn)中并未定義谭网,通常用來描述行為汪厨。
for循環(huán)(for Loops)
在for
循環(huán)中,你可以循環(huán)取得數(shù)組或是數(shù)組類似對象的值愉择,譬如arguments
和HTMLCollection
對象劫乱。通常的循環(huán)形式如下:
// 次佳的循環(huán)
for (var i = 0; i < myarray.length; i++) {
// 使用myarray[i]做點什么
}
這種形式的循環(huán)的不足在于每次循環(huán)的時候數(shù)組的長度都要去獲取下。這回降低你的代碼锥涕,尤其當(dāng)myarray
不是數(shù)組衷戈,而是一個HTMLCollection
對象的時候。
HTMLCollections
指的是DOM方法返回的對象层坠,例如:
document.getElementsByName()
document.getElementsByClassName()
document.getElementsByTagName()
還有其他一些HTMLCollections
殖妇,這些是在DOM標(biāo)準(zhǔn)之前引進(jìn)并且現(xiàn)在還在使用的。有:
document.images: 頁面上所有的圖片元素
document.links : 所有a標(biāo)簽元素
document.forms : 所有表單
document.forms[0].elements : 頁面上第一個表單中的所有域
集合的麻煩在于它們實時查詢基本文檔(HTML頁面)破花。這意味著每次你訪問任何集合的長度谦趣,你要實時查詢DOM,而DOM操作一般都是比較昂貴的座每。
這就是為什么當(dāng)你循環(huán)獲取值時前鹅,緩存數(shù)組(或集合)的長度是比較好的形式,正如下面代碼顯示的:
for (var i = 0, max = myarray.length; i < max; i++) {
// 使用myarray[i]做點什么
}
這樣峭梳,在這個循環(huán)過程中舰绘,你只檢索了一次長度值。
在所有瀏覽器下葱椭,循環(huán)獲取內(nèi)容時緩存HTMLCollections
的長度是更快的捂寿,2倍(Safari3)到190倍(IE7)之間。//zxx:此數(shù)據(jù)貌似很老孵运,僅供參考
注意到秦陋,當(dāng)你明確想要修改循環(huán)中集合的時候(例如,添加更多的DOM元素)治笨,你可能更喜歡長度更新而不是常量驳概。
伴隨著單var
形式粪小,你可以把變量從循環(huán)中提出來,就像下面這樣:
function looper() {
var i = 0,
max,
myarray = [];
// ...
for (i = 0, max = myarray.length; i < max; i++) {
// 使用myarray[i]做點什么
}
}
這種形式具有一致性的好處抡句,因為你堅持了單一var
形式探膊。不足在于當(dāng)重構(gòu)代碼的時候,復(fù)制和粘貼整個循環(huán)有點困難待榔。例如逞壁,你從一個函數(shù)復(fù)制了一個循環(huán)到另一個函數(shù),你不得不去確定你能夠把i
和max
引入新的函數(shù)(如果在這里沒有用的話锐锣,很有可能你要從原函數(shù)中把它們刪掉)腌闯。
最后,一個需要對循環(huán)進(jìn)行調(diào)整的是使用下面表達(dá)式之一來替換i++
雕憔。
i = i + 1
i += 1
JSLint提示您這樣做姿骏,原因是++
和–-
促進(jìn)了“過分棘手(excessive trickiness)”。//zxx:這里比較難翻譯斤彼,我想本意應(yīng)該是讓代碼變得更加的棘手
如果你直接無視它分瘦,JSLint的plusplus
選項會是false
(默認(rèn)是default)。
還有兩種變化的形式琉苇,其又有了些微改進(jìn)嘲玫,因為:
- 少了一個變量(無max)
- 向下數(shù)到0,通常更快并扇,因為和0做比較要比和數(shù)組長度或是其他不是0的東西作比較更有效率
第一種變化的形式:
var i, myarray = [];
for (i = myarray.length; i–-;) {
// 使用myarray[i]做點什么
}
第二種使用while
循環(huán):
var myarray = [],
i = myarray.length;
while (i–-) {
// 使用myarray[i]做點什么
}
這些小的改進(jìn)只體現(xiàn)在性能上去团,此外JSLint會對使用i–-
加以抱怨。
for-in循環(huán)(for-in Loops)
for-in
循環(huán)應(yīng)該用在非數(shù)組對象的遍歷上穷蛹,使用for-in
進(jìn)行循環(huán)也被稱為“枚舉”土陪。
從技術(shù)上將,你可以使用for-in
循環(huán)數(shù)組(因為JavaScript中數(shù)組也是對象)肴熏,但這是不推薦的鬼雀。因為如果數(shù)組對象已被自定義的功能增強(qiáng),就可能發(fā)生邏輯錯誤扮超。另外取刃,在for-in
中蹋肮,屬性列表的順序(序列)是不能保證的出刷。所以最好數(shù)組使用正常的for循環(huán),對象使用for-in
循環(huán)坯辩。
有個很重要的hasOwnProperty()
方法馁龟,當(dāng)遍歷對象屬性的時候可以過濾掉從原型鏈上下來的屬性。
思考下面一段代碼:
// 對象
var man = {
hands: 2,
legs: 2,
heads: 1
};
// 在代碼的某個地方
// 一個方法添加給了所有對象
if (typeof Object.prototype.clone === "undefined") {
Object.prototype.clone = function () {};
}
在這個例子中漆魔,我們有一個使用對象字面量定義的名叫man
的對象坷檩。在man
定義完成后的某個地方却音,在對象原型上增加了一個很有用的名叫clone()
的方法。此原型鏈?zhǔn)菍崟r的矢炼,這就意味著所有的對象自動可以訪問新的方法系瓢。為了避免枚舉man
的時候出現(xiàn)clone()
方法,你需要應(yīng)用hasOwnProperty()
方法過濾原型屬性句灌。如果不做過濾夷陋,會導(dǎo)致clone()
函數(shù)顯示出來,在大多數(shù)情況下這是不希望出現(xiàn)的胰锌。
// 1.
// for-in 循環(huán)
for (var i in man) {
if (man.hasOwnProperty(i)) { // 過濾
console.log(i, ":", man[i]);
}
}
/* 控制臺顯示結(jié)果
hands : 2
legs : 2
heads : 1
*/
// 2.
// 反面例子:
// for-in loop without checking hasOwnProperty()
for (var i in man) {
console.log(i, ":", man[i]);
}
/*
控制臺顯示結(jié)果
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]);
}
}
嚴(yán)格來說,不使用
hasOwnProperty()
并不是一個錯誤格带。根據(jù)任務(wù)以及你對代碼的自信程度撤缴,你可以跳過它以提高些許的循環(huán)速度。但是當(dāng)你對當(dāng)前對象內(nèi)容(和其原型鏈)不確定的時候叽唱,添加hasOwnProperty()
更加保險些腹泌。
格式化的變化(通不過JSLint)會直接忽略掉花括號,把if語句放到同一行上尔觉。其優(yōu)點在于循環(huán)語句讀起來就像一個完整的想法(每個元素都有一個自己的屬性”X”凉袱,使用”X”干點什么):
// 警告: 通不過JSLint檢測
var i, hasOwn = Object.prototype.hasOwnProperty;
for (i in man) if (hasOwn.call(man, i)) { // 過濾
console.log(i, ":", man[i]);
}
(不)擴(kuò)展內(nèi)置原型((Not) Augmenting Built-in Prototypes)
擴(kuò)增構(gòu)造函數(shù)的prototype
屬性是個很強(qiáng)大的增加功能的方法,但有時候它太強(qiáng)大了侦铜。
增加內(nèi)置的構(gòu)造函數(shù)原型(如Object()
, Array()
, 或Function()
)挺誘人的专甩,但是這嚴(yán)重降低了可維護(hù)性,因為它讓你的代碼變得難以預(yù)測钉稍。使用你代碼的其他開發(fā)人員很可能更期望使用內(nèi)置的JavaScript方法來持續(xù)不斷地工作涤躲,而不是你另加的方法。
另外贡未,屬性添加到原型中种樱,可能會導(dǎo)致不使用hasOwnProperty
屬性時在循環(huán)中顯示出來,這會造成混亂俊卤。
因此嫩挤,不增加內(nèi)置原型是最好的。你可以指定一個規(guī)則消恍,僅當(dāng)下面的條件均滿足時例外:
- 可以預(yù)期將來的ECMAScript版本岂昭,或是JavaScript實現(xiàn)將一直將此功能當(dāng)作內(nèi)置方法來實現(xiàn)。例如狠怨,你可以添加ECMAScript 5中描述的方法约啊,一直到各個瀏覽器都迎頭趕上邑遏。這種情況下,你只是提前定義了有用的方法恰矩。
- 如果您檢查您的自定義屬性或方法已不存在——也許已經(jīng)在代碼的其他地方實現(xiàn)记盒,或已經(jīng)是你支持的瀏覽器JavaScript引擎部分。
- 你清楚地文檔記錄并和團(tuán)隊交流了變化外傅。
如果這三個條件得到滿足孽鸡,你可以給原型進(jìn)行自定義的添加,形式如下:
if (typeof Object.protoype.myMethod !== "function") {
Object.protoype.myMethod = function () {
// 實現(xiàn)...
};
}
switch形式(switch Pattern)
你可以通過類似下面形式的switch語句栏豺,增強(qiáng)可讀性和健壯性:
var inspect_me = 0,
result = '';
switch (inspect_me) {
case 0:
result = "zero";
break;
case 1:
result = "one";
break;
default:
result = "unknown";
}
這個簡單的例子中所遵循的風(fēng)格約定如下:
- 每個case和switch對齊(花括號縮進(jìn)規(guī)則除外)
- 每個case中代碼縮進(jìn)
- 每個case以break清除結(jié)束
- 避免貫穿(故意忽略break)彬碱。如果你非常確信貫穿是最好的方法,務(wù)必記錄此情況奥洼,因為對于有些閱讀人而言巷疼,它們可能看起來是錯誤的。
- 以default結(jié)束switch:確绷榻保總有健全的結(jié)果嚼沿,即使無情況匹配。
避免隱式類型轉(zhuǎn)換(Avoiding Implied Typecasting)
JavaScript的變量在比較的時候會隱式類型轉(zhuǎn)換瓷患。這就是為什么一些諸如:false == 0
或“” == 0
返回的結(jié)果是true
骡尽。為避免引起混亂的隱含類型轉(zhuǎn)換,在你比較值和表達(dá)式類型的時候始終使用===
和!==
操作符擅编。
var zero = 0;
if (zero === false) {
// 不執(zhí)行攀细,因為zero為0, 而不是false
}
// 反面示例
if (zero == false) {
// 執(zhí)行了...
}
還有另外一種思想觀點認(rèn)為==
就足夠了===
是多余的。例如爱态,當(dāng)你使用typeof
你就知道它會返回一個字符串谭贪,所以沒有使用嚴(yán)格相等的理由。然而锦担,JSLint要求嚴(yán)格相等俭识,它使代碼看上去更有一致性,可以降低代碼閱讀時的精力消耗洞渔。(“==
是故意的還是一個疏漏套媚?”)
避免(Avoiding) eval()
如果你現(xiàn)在的代碼中使用了eval()
,記住該咒語“eval()
是魔鬼”磁椒。此方法接受任意的字符串堤瘤,并當(dāng)作JavaScript代碼來處理。當(dāng)有問題的代碼是事先知道的(不是運行時確定的)衷快,沒有理由使用eval()
宙橱。如果代碼是在運行時動態(tài)生成姨俩,有一個更好的方式不使用eval
而達(dá)到同樣的目標(biāo)蘸拔。例如师郑,用方括號表示法來訪問動態(tài)屬性,會更好更簡單:
// 反面示例
var property = "name";
alert(eval("obj." + property));
// 更好的
var property = "name";
alert(obj[property]);
使用eval()
也帶來了安全隱患调窍,因為被執(zhí)行的代碼(例如從網(wǎng)絡(luò)來)可能已被篡改宝冕。這是個很常見的反面教材,當(dāng)處理Ajax請求得到的JSON 相應(yīng)的時候邓萨。在這些情況下地梨,最好使用JavaScript內(nèi)置方法來解析JSON相應(yīng),以確保安全和有效缔恳。若瀏覽器不支持JSON.parse()
宝剖,你可以使用來自JSON.org
的庫。
同樣重要的是要記住歉甚,給setInterval()
, setTimeout()
和Function()
構(gòu)造函數(shù)傳遞字符串万细,大部分情況下,與使用eval()
是類似的纸泄,因此要避免赖钞。在幕后,JavaScript仍需要評估和執(zhí)行你給程序傳遞的字符串:
// 反面示例
setTimeout("myFunc()", 1000);
setTimeout("myFunc(1, 2, 3)", 1000);
// 更好的
setTimeout(myFunc, 1000);
setTimeout(function () {
myFunc(1, 2, 3);
}, 1000);
使用新的Function()
構(gòu)造就類似于eval()
聘裁,應(yīng)小心接近雪营。這可能是一個強(qiáng)大的構(gòu)造,但往往被誤用衡便。如果你絕對必須使用eval()
献起,你可以考慮使用new Function()
代替。有一個小的潛在好處镣陕,因為在新Function()
中作代碼評估是在局部函數(shù)作用域中運行征唬,所以代碼中任何被評估的通過var
定義的變量都不會自動變成全局變量。另一種方法來阻止自動全局變量是封裝eval()
調(diào)用到一個即時函數(shù)中茁彭。
考慮下面這個例子总寒,這里僅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構(gòu)造不同的是理肺,eval()
可以干擾作用域鏈摄闸,而Function()
更安分守己些。不管你在哪里執(zhí)行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()
下的數(shù)值轉(zhuǎn)換(Number Conversions with parseInt())
使用parseInt()
你可以從字符串中獲取數(shù)值,該方法接受另一個基數(shù)參數(shù)摩桶,這經(jīng)常省略桥状,但不應(yīng)該。當(dāng)字符串以”0″開頭的時候就有可能會出問題硝清,例如辅斟,部分時間進(jìn)入表單域,在ECMAScript 3中芦拿,開頭為”0
″的字符串被當(dāng)做8進(jìn)制處理了士飒,但這已在ECMAScript 5中改變了。為了避免矛盾和意外的結(jié)果蔗崎,總是指定基數(shù)參數(shù)酵幕。
var month = "06",
year = "09";
month = parseInt(month, 10);
year = parseInt(year, 10);
此例中,如果你忽略了基數(shù)參數(shù)缓苛,如parseInt(year)
裙盾,返回的值將是0,因為“09”被當(dāng)做8進(jìn)制(好比執(zhí)行 parseInt( year, 8 )
)他嫡,而09在8進(jìn)制中不是個有效數(shù)字番官。
替換方法是將字符串轉(zhuǎn)換成數(shù)字,包括:
+"08" // 結(jié)果是 8
Number("08") // 8
這些通掣质簦快于parseInt()
徘熔,因為parseInt()
方法,顧名思意淆党,不是簡單地解析與轉(zhuǎn)換酷师。但是,如果你想輸入例如“08 hello
”染乌,parseInt()
將返回數(shù)字山孔,而其它以NaN告終。
編碼規(guī)范(Coding Conventions)
建立和遵循編碼規(guī)范是很重要的荷憋,這讓你的代碼保持一致性台颠,可預(yù)測,更易于閱讀和理解勒庄。一個新的開發(fā)者加入這個團(tuán)隊可以通讀規(guī)范串前,理解其它團(tuán)隊成員書寫的代碼,更快上手干活实蔽。
許多激烈的爭論發(fā)生會議上或是郵件列表上荡碾,問題往往針對某些代碼規(guī)范的特定方面(例如代碼縮進(jìn),是Tab制表符鍵還是space空格鍵)局装。如果你組織中建議采用規(guī)范的坛吁,準(zhǔn)備好面對各種反對的劳殖,或是聽起來不同但很強(qiáng)烈的觀點。要記住拨脉,建立和堅定不移地遵循規(guī)范要比糾結(jié)于規(guī)范的細(xì)節(jié)重要的多哆姻。
縮進(jìn)(Indentation)
代碼沒有縮進(jìn)基本上就不能讀了。唯一糟糕的事情就是不一致的縮進(jìn)女坑,因為它看上去像是遵循了規(guī)范填具,但是可能一路上伴隨著混亂和驚奇统舀。重要的是規(guī)范地使用縮進(jìn)匆骗。
一些開發(fā)人員更喜歡用tab制表符縮進(jìn),因為任何人都可以調(diào)整他們的編輯器以自己喜歡的空格數(shù)來顯示Tab誉简。有些人喜歡空格——通常四個碉就,這都無所謂,只要團(tuán)隊每個人都遵循同一個規(guī)范就好了闷串。這本書瓮钥,例如,使用四個空格縮進(jìn)烹吵,這也是JSLint中默認(rèn)的縮進(jìn)锈死。
什么應(yīng)該縮進(jìn)呢舆逃?規(guī)則很簡單——花括號里面的東西。這就意味著函數(shù)體,循環(huán) (do
, while
, for
, for-in
)蛤袒,if
,switch
秀存,以及對象字面量中的對象屬性羔杨。下面的代碼就是使用縮進(jìn)的示例:
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)
花括號(亦稱大括號,下同)應(yīng)總被使用窿吩,即使在它們?yōu)榭蛇x的時候茎杂。技術(shù)上將,在in或是for中如果語句僅一條纫雁,花括號是不需要的煌往,但是你還是應(yīng)該總是使用它們,這會讓代碼更有持續(xù)性和易于更新轧邪。
想象下你有一個只有一條語句的for循環(huán)携冤,你可以忽略花括號,而沒有解析的錯誤闲勺。
// 糟糕的實例
for (var i = 0; i < 10; i += 1)
alert(i);
但是曾棕,如果,后來菜循,主體循環(huán)部分又增加了行代碼翘地?
// 糟糕的實例
for (var i = 0; i < 10; i += 1)
alert(i);
alert(i + " is " + (i % 2 ? "odd" : "even"));
第二個alert已經(jīng)在循環(huán)之外,縮進(jìn)可能欺騙了你。為了長遠(yuǎn)打算衙耕,最好總是使用花括號昧穿,即時值一行代碼:
// 好的實例
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)
開發(fā)人員對于左大括號的位置有著不同的偏好——在同一行或是下一行。
if (true) {
alert("It's TRUE!");
}
或
if (true)
{
alert("It's TRUE!");
}
這個實例中橙喘,仁者見仁智者見智时鸵,但也有個案,括號位置不同會有不同的行為表現(xiàn)厅瞎。這是因為分號插入機(jī)制(semicolon insertion mechanism)——JavaScript是不挑剔的饰潜,當(dāng)你選擇不使用分號結(jié)束一行代碼時JavaScript會自己幫你補(bǔ)上。這種行為可能會導(dǎo)致麻煩和簸,如當(dāng)你返回對象字面量彭雾,而左括號卻在下一行的時候:
// 警告: 意外的返回值
function func() {
return
// 下面代碼不執(zhí)行
{
name : "Batman"
}
}
如果你希望函數(shù)返回一個含有name
屬性的對象,你會驚訝锁保。由于隱含分號薯酝,函數(shù)返回undefined
。前面的代碼等價于:
// 警告: 意外的返回值
function func() {
return undefined;
// 下面代碼不執(zhí)行
{
name : "Batman"
}
}
總之爽柒,總是使用花括號吴菠,并始終把在與之前的語句放在同一行:
function func() {
return {
name : "Batman"
};
}
關(guān)于分號注:就像使用花括號,你應(yīng)該總是使用分號浩村,即使他們可由JavaScript解析器隱式創(chuàng)建做葵。這不僅促進(jìn)更科學(xué)和更嚴(yán)格的代碼,而且有助于解決存有疑惑的地方穴亏,就如前面的例子顯示蜂挪。
空格(White Space)
空格的使用同樣有助于改善代碼的可讀性和一致性。在寫英文句子的時候嗓化,在逗號和句號后面會使用間隔棠涮。在JavaScript中,你可以按照同樣的邏輯在列表模樣表達(dá)式(相當(dāng)于逗號)和結(jié)束語句(相對于完成了“想法”)后面添加間隔刺覆。
適合使用空格的地方包括:
- for循環(huán)分號分開后的的部分:如
for (var i = 0; i < 10; i += 1) {...}
- for循環(huán)中初始化的多變量(i和max):
for (var i = 0, max = 10; i < max; i += 1) {...}
- 分隔數(shù)組項的逗號的后面:
var a = [1, 2, 3];
- 對象屬性逗號的后面以及分隔屬性名和屬性值的冒號的后面:
var o = {a: 1, b: 2};
- 限定函數(shù)參數(shù):
myFunc(a, b, c)
- 函數(shù)聲明的花括號的前面:
function myFunc() {}
- 匿名函數(shù)表達(dá)式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;
}
//zxx:我就琢磨著這正面和反面例子不長得一樣嗎...原文就是如此谦屑,我也不好擅自改動驳糯。
最后需要注意的一個空格——花括號間距。最好使用空格:
- 函數(shù)氢橙、if-else語句酝枢、循環(huán)、對象字面量的左花括號的前面({)
- else或while之間的右花括號(})
空格使用的一點不足就是增加了文件的大小悍手,但是壓縮無此問題帘睦。
有一個經(jīng)常被忽略的代碼可讀性方面是垂直空格的使用袍患。你可以使用空行來分隔代碼單元,就像是文學(xué)作品中使用段落分隔一樣竣付。
命名規(guī)范(Naming Conventions)
另一種方法诡延,讓你的代碼更具可預(yù)測性和可維護(hù)性是采用命名規(guī)范。這就意味著你需要用同一種形式古胆,給你的變量和函數(shù)命名肆良。
下面是建議的一些命名規(guī)范,你可以原樣采用逸绎,也可以根據(jù)自己的喜好作調(diào)整惹恃。同樣,遵循規(guī)范要比規(guī)范是什么更重要桶良。
以大寫字母寫構(gòu)造函數(shù)(Capitalizing Constructors)
JavaScript并沒有類座舍,但有new
調(diào)用的構(gòu)造函數(shù):
var adam = new Person();
因為構(gòu)造函數(shù)仍僅僅是函數(shù)沮翔,僅看函數(shù)名就可以幫助告訴你這應(yīng)該是一個構(gòu)造函數(shù)還是一個正常的函數(shù)陨帆。
命名構(gòu)造函數(shù)時,首字母大寫具有暗示作用采蚀,使用小寫命名的函數(shù)和方法不應(yīng)該使用new
調(diào)用:
function MyConstructor() {...}
function myFunction() {...}
分隔單詞(Separating Words)
當(dāng)你的變量或是函數(shù)名有多個單詞的時候疲牵,最好單詞的分離遵循統(tǒng)一的規(guī)范,有一個常見的做法被稱作“駝峰(Camel)命名法”榆鼠,就是單詞小寫纲爸,每個單詞的首字母大寫。
對于構(gòu)造函數(shù)妆够,可以使用大駝峰式命名法(upper camel case)识啦,如MyConstructor()
。對于函數(shù)和方法名稱神妹,你可以使用小駝峰式命名法(lower camel case)颓哮,像是myFunction()
, calculateArea()
和getFirstName()
。
要是變量不是函數(shù)呢鸵荠?開發(fā)者通常使用小駝峰式命名法冕茅,但還有另外一種做法,就是所有單詞小寫以下劃線連接:例如蛹找,first_name
, favorite_bands
, 和old_company_name
姨伤,這種標(biāo)記法幫你直觀地區(qū)分函數(shù)和其他標(biāo)識——原型和對象。
ECMAScript的屬性和方法均使用Camel標(biāo)記法庸疾,盡管多字的屬性名稱是罕見的(正則表達(dá)式對象的lastIndex
和ignoreCase
屬性)乍楚。
其它命名形式(Other Naming Patterns)
有時,開發(fā)人員使用命名規(guī)范來彌補(bǔ)或替代語言特性届慈。
例如徒溪,JavaScript中沒有定義常量的方法(盡管有些內(nèi)置的像Number
, MAX_VALUE
)凌箕,所以開發(fā)者都采用全部單詞大寫的規(guī)范,來命名這個程序生命周期中都不會改變的變量词渤,如:
// 珍貴常數(shù)牵舱,只可遠(yuǎn)觀
var PI = 3.14,
MAX_WIDTH = 800;
還有另外一個完全大寫的慣例:全局變量名字全部大寫。全部大寫命名全局變量可以加強(qiáng)減小全局變量數(shù)量的實踐缺虐,同時讓它們易于區(qū)分芜壁。
另外一種使用規(guī)范來模擬功能的是私有成員。雖然可以在JavaScript中實現(xiàn)真正的私有高氮,但是開發(fā)者發(fā)現(xiàn)僅僅使用一個下劃線前綴來表示一個私有屬性或方法會更容易些慧妄。考慮下面的例子:
var person = {
getName: function () {
return this._getFirst() + ' ' + this._getLast();
},
_getFirst: function () {
// ...
},
_getLast: function () {
// ...
}
};
在此例中剪芍,getName()
就表示公共方法塞淹,部分穩(wěn)定的API。而_getFirst()
和_getLast()
則表明了私有罪裹。它們?nèi)匀皇钦5墓卜椒ūテ铡5鞘褂孟聞澗€前綴來警告person對象的使用者,這些方法在下一個版本中時不能保證工作的状共,是不能直接使用的套耕。注意,JSLint有些不鳥下劃線前綴峡继,除非你設(shè)置了noman
選項為:false冯袍。
下面是一些常見的_private
規(guī)范:
- 使用尾下劃線表示私有,如
name_
和getElements_()
- 使用一個下劃線前綴表
_protected
(保護(hù))屬性碾牌,兩個下劃線前綴表示__private
(私有)屬性 - Firefox中一些內(nèi)置的變量屬性不屬于該語言的技術(shù)部分康愤,使用兩個前下劃線和兩個后下劃線表示,如:
__proto__
和__parent__
舶吗。
注釋(Writing Comments)
你必須注釋你的代碼征冷,即使不會有其他人向你一樣接觸它。通常裤翩,當(dāng)你深入研究一個問題资盅,你會很清楚的知道這個代碼是干嘛用的,但是踊赠,當(dāng)你一周之后再回來看的時候呵扛,想必也要耗掉不少腦細(xì)胞去搞明白到底怎么工作的。
很顯然筐带,注釋不能走極端:每個單獨變量或是單獨一行今穿。但是,你通常應(yīng)該記錄所有的函數(shù)伦籍,它們的參數(shù)和返回值蓝晒,或是任何不尋常的技術(shù)和方法腮出。要想到注釋可以給你代碼未來的閱讀者以諸多提示;閱讀者需要的是(不要讀太多的東西)僅注釋和函數(shù)屬性名來理解你的代碼芝薇。例如胚嘲,當(dāng)你有五六行程序執(zhí)行特定的任務(wù),如果你提供了一行代碼目的洛二,以及為什么在這里的描述的話馋劈,閱讀者就可以直接跳過這段細(xì)節(jié)。沒有硬性規(guī)定注釋代碼比晾嘶,代碼的某些部分(如正則表達(dá)式)可能注釋要比代碼多妓雾。
最重要的習(xí)慣,然而也是最難遵守的垒迂,就是保持注釋的及時更新械姻,因為過時的注釋比沒有注釋更加的誤導(dǎo)人。
關(guān)于作者(About the Author)
Stoyan Stefanov是Yahoo!web開發(fā)人員机断,多個O'Reilly書籍的作者楷拳、投稿者和技術(shù)評審。他經(jīng)常在會議和他的博客www.phpied.com上發(fā)表web開發(fā)主題的演講毫缆。Stoyan還是smush.it圖片優(yōu)化工具的創(chuàng)造者唯竹,YUI貢獻(xiàn)者乐导,雅虎性能優(yōu)化工具YSlow 2.0的架構(gòu)設(shè)計師苦丁。