第二章 高質量JavaScript基本要點
本章將對一些實質內容展開討論,這些內容包括最佳實踐、模式和編寫高質量JavaScript代碼的習慣,好比避免全局變量、使用單var聲明、循環中的length預緩存、遵照編碼約定等等。本章還包括一些非必要的編程習慣,但更多的關注點將放在整體的代碼建立過程上,包括撰寫API文檔、組織相互評審以及使用JSLint。這些習慣和最佳實踐能夠幫助你寫出更好的、更易讀的和可維護的代碼,當幾個月後或數年後再重讀你的代碼時,你就會深有體會了。java
編寫可維護的代碼程序員
修復軟件bug成本很高,並且隨着時間的推移,它們形成的損失也愈來愈大,特別是在已經打包發佈了的軟件發現了bug的時候。固然最好是發現bug馬上解決掉,但前提是你對你的代碼依然很熟悉,不然當你轉身投入到另一個項目的開發中後,根本不記得當初代碼的模樣了。過了一段時間後你再去閱讀當初的代碼你須要:web
時間來從新學習並理解問題
時間去理解問題相關的代碼
對大型項目或者公司來講還有一個不得不考慮的問題,就是解決這個bug的人和製造這個bug的人每每不是同一我的。所以減小理解代碼所需的時間成本就顯得很是重要,無論是隔了很長時間重讀本身的代碼仍是閱讀團隊內其餘人的代碼。這對於公司的利益底線和工程師的幸福指數一樣重要,由於每一個人都寧願去開發新的項目而不肯花不少時間和精力去維護舊代碼。正則表達式
另一個軟件開發中的廣泛現象是,在讀代碼上花的時間要遠遠超過寫代碼的時間。經常當你專一於某個問題的時候,你會坐下來用一下午的時間產出大量的代碼。當時的場景下代碼是能夠正常運行的,但當應用趨於成熟,會有不少因素促使你重讀代碼、改進代碼或對代碼作微調。好比:算法
發現了bug
須要給應用添加新需求
須要將應用遷移到新的平臺中運行(好比當市場中出現了新的瀏覽器時)
代碼重構
因爲架構更改或者更換另外一種語言致使代碼重寫
這些不肯定因素帶來的後果是,少數人花幾小時寫的代碼須要不少人花幾個星期去閱讀它。所以,建立可維護的代碼對於一個成功的應用來講相當重要。編程
可維護的代碼意味着代碼是:數組
可讀的
一致的
可預測的
看起來像是同一我的寫的
有文檔的
本章接下來的部分會對這幾點深刻講解。瀏覽器
減小全局對象緩存
JavaScript 使用函數來管理做用域,在一個函數內定義的變量稱做「局部變量」,局部變量在函數外部是不可見的。另外一方面,「全局變量」是不在任何函數體內部聲明的變量,或者是直接使用而未明的變量。安全
每個JavaScript運行環境都有一個「全局對象」,不在任何函數體內使用this就能夠得到對這個全局對象的引用。你所建立的每個全局變量都是這個全局對象的屬性。爲了方便起見,瀏覽器都會額外提供一個全局對象的屬性window,(經常)用以指向全局對象自己。下面的示例代碼中展現瞭如何在瀏覽器中建立或訪問全局變量:
myglobal = 「hello」; // antipattern
console.log(myglobal); // 「hello」
console.log(window.myglobal); // 「hello」
console.log(window[「myglobal」]); // 「hello」
console.log(this.myglobal); // 「hello」
全局對象帶來的困擾
全局變量的問題是,它們在JavaScript代碼執行期間或者整個web頁面中始終是可見的。它們存在於同一個命名空間中,所以命名衝突的狀況時有發生,畢竟在應用程序的不一樣模塊中,常常會出於某種目的定義相同的全局變量。
一樣,經常網頁中所嵌入的代碼並非這個網頁的開發者所寫,好比:
網頁中使用了第三方的JavaScript庫
網頁中使用了廣告代碼
網頁中使用了用以分析流量和點擊率的第三方統計代碼
網頁中使用了不少組件,掛件和按鈕等等
假設某一段第三方提供的腳本定義了一個全局變量result。隨後你在本身寫的某個函數中也定義了一個全局變量result。這時,第二個變量就會覆蓋第一個,這時就會致使第三方腳本中止工做。
所以,爲了讓你的腳本和這個頁面中的其餘腳本和諧相處,要儘量少的使用全局變量,這一點很是重要。本書隨後的章節中會講到一些減小全局變量的技巧和策略,好比使用命名空間或者當即執行的匿名函數等,但減小全局變量最有效的方法是堅持使用var來聲明變量。
因爲JavaScript的特色,咱們常常有意無心的建立全局變量,畢竟在JavaScript中建立全局變量實在太簡單了。首先,你能夠不聲明而直接使用變量,再者,JavaScirpt中具備「隱式全局對象」的概念,也就是說任何不經過var聲明(譯註:在JavaScript1.7及之後的版本中,能夠經過let來聲明塊級做用域的變量)的變量都會成爲全局對象的一個屬性(能夠把它們看成全局變量)。看一下下面這段代碼:
function sum(x, y) {
// antipattern: implied global
result = x + y;
return result;
}
這段代碼中,咱們直接使用了result而沒有事先聲明它。這段代碼是可以正常工做的,但在調用這個方法以後,會產生一個全局變量result,這會帶來其餘問題。
解決辦法是,老是使用var來聲明變量,下面代碼就是改進了的sum()函數:
function sum(x, y) {
var result = x + y;
return result;
}
這裏咱們要注意一種反模式,就是在var聲明中經過鏈式賦值的方法建立全局變量。在下面這個代碼片斷中,a是局部變量,但b是全局變量,而做者的意圖顯然不是如此:
// antipattern, do not use
function foo() {
var a = b = 0;
// …
}
爲何會這樣?由於這裏的計算順序是從右至左的。首先計算表達式b=0,這裏的b是未聲明的,這個表達式的值是0,而後經過var建立了局部變量a,並賦值爲0。換言之,能夠等價的將代碼寫成這樣:
var a = (b = 0);
若是變量b已經被聲明,這種鏈式賦值的寫法是ok的,不會意外的建立全局變量,好比:
function foo() {
var a, b;
// …
a = b = 0; // both local
}
避免使用全局變量的另外一個緣由是出於可移植性考慮的,若是你但願將你的代碼運行於不一樣的平臺環境(宿主),使用全局變量則很是危險。頗有可能你無心間建立的某個全局變量在當前的平臺環境中是不存在的,你認爲能夠安全的使用,而在其餘的環境中倒是存在的。
忘記var時的反作用
隱式的全局變量和顯式定義的全局變量之間有着細微的差異,差異在於經過delete來刪除它們的時候表現不一致。
經過var建立的全局變量(在任何函數體以外建立的變量)不能被刪除。
沒有用var建立的隱式全局變量(不考慮函數內的狀況)能夠被刪除。
也就是說,隱式全局變量並不算是真正的變量,但他們是全局對象的屬性成員。屬性是能夠經過delete運算符刪除的,而變量不能夠被刪除:
// define three globals
var global_var = 1;
global_novar = 2; // antipattern
(function () {
global_fromfunc = 3; // antipattern
}());
// attempt to delete
delete global_var; // false
delete global_novar; // true
delete global_fromfunc; // true
// test the deletion
typeof global_var; // 「number」
typeof global_novar; // 「undefined」
typeof global_fromfunc; // 「undefined」
在ES5嚴格模式中,給未聲明的變量賦值會報錯(好比這段代碼中提到的兩個反模式)。
訪問全局對象
在瀏覽器中,咱們能夠隨時隨地經過window屬性來訪問全局對象(除非你定義了一個名叫window的局部變量)。但換一個運行環境這個方便的window可能就換成了別的名字(甚至根本就被禁止訪問全局對象了)。若是不想經過這種寫死window的方式來獲得全局變量,有一個辦法,你能夠在任意層次嵌套的函數做用域內執行:
var global = (function () {
return this;
}());
這種方式老是能夠獲得全局對象,由於在被看成函數執行的函數體內(而不是被看成構造函數執行的函數體內),this老是指向全局對象。但這種狀況在ECMAScript5的嚴格模式中行不通,所以在嚴格模式中你不得不尋求其餘的替代方案。好比,若是你在開發一個庫,你會將你的代碼包裝在一個當即執行的匿名函數中(在第四章會講到),而後從全局做用域中給這個匿名函數傳入一個指向this的參數。
單 var 模式
在函數的頂部使用一個單獨的var語句是很是推薦的一種模式,它有以下一些好處:
在同一個位置能夠查找到函數所需的全部變量
避免當在變量聲明以前使用這個變量時產生的邏輯錯誤(參照下一小節「聲明提早:分散的 var 帶來的問題」)
提醒你不要忘記聲明變量,順便減小潛在的全局變量
代碼量更少(輸入更少且更易作代碼優化)
單var模式看起來像這樣:
function func() {
var a = 1,
b = 2,
sum = a + b,
myobject = {},
i,
j;
// function body…
}
你能夠使用一個var語句來聲明多個變量,變量之間用逗號分隔。也能夠在這個語句中加入變量的初始化,這是一個很是好的實踐。這種方式能夠避免邏輯錯誤(全部未初始化的變量都被聲明瞭,且值爲undefined)並增長了代碼的可讀性。過段時間後再看這段代碼,你會體會到聲明不一樣類型變量的慣用名稱,好比,你一眼就可看出某個變量是對象仍是整數。
你能夠在聲明變量時多作一些額外的工做,好比在這個例子中就寫了sum=a+b這種代碼。另外一個例子就是當代碼中用到對DOM元素時,你能夠把對DOM的引用賦值給一些變量,這一步就能夠放在一個單獨的聲明語句中,好比下面這段代碼:
function updateElement() {
var el = document.getElementById(「result」),
style = el.style;
// do something with el and style…
}
聲明提早:分散的 var 帶來的問題
JavaScript 中是容許在函數的任意地方寫任意多個var語句的,其實至關於在函數體頂部聲明變量,這種現象被稱爲「變量提早」,當你在聲明以前使用這個變量時,可能會形成邏輯錯誤。對於JavaScript來講,一旦在某個做用域(同一個函數內)裏聲明瞭一個變量,這個變量在整個做用域內都是存在的,包括在var聲明語句以前。看一下這個例子:
// antipattern
myname = 「global」; // global variable
function func() {
alert(myname); // 「undefined」
var myname = 「local」;
alert(myname); // 「local」
}
func();
這個例子中,你可能指望第一個alert()彈出「global」,第二個alert()彈出「local」。這種結果看起來是合乎常理的,由於在第一個alert執行時,myname尚未聲明,這時就應該「尋找」全局變量中的myname。但實際狀況並非這樣,第一個alert彈出「undefined」,由於myname已經在函數內有聲明瞭(儘管聲明語句在後面)。全部的變量聲明都提早到了函數的頂部。所以,爲了不相似帶有「歧義」的程序邏輯,最好在使用以前一塊兒聲明它們。
上一個代碼片斷等價於下面這個代碼片斷:
myname = 「global」; // global variable
function func() {
var myname; // same as -> var myname = undefined;
alert(myname); // 「undefined」
myname = 「local」;
alert(myname); // 「local」
}
func();
這裏有必要對「變量提早」做進一步補充,實際上從JavaScript引擎的工做機制上看,這個過程稍微有點複雜。代碼處理通過了兩個階段,第一階段是建立變量、函數和參數,這一步是預編譯的過程,它會掃描整段代碼的上下文。第二階段是代碼的運行,這一階段將建立函數表達式和一些非法的標識符(未聲明的變量)。從實用性角度來說,咱們更願意將這兩個階段歸成一個概念「變量提早」,儘管這個概念並無在ECMAScript標準中定義,但咱們經常用它來解釋預編譯的行爲過程。
for 循環
在for循環中,能夠對數組或相似數組的對象(好比arguments和HTMLCollection對象)做遍歷,最普通的for循環模式形如:
// sub-optimal loop
for (var i = 0; i < myarray.length; i++) {
// do something with myarray[i]
}
這種模式的問題是,每次遍歷都會訪問數組的length屬性。這下降了代碼運行效率,特別是當myarray並非一個數組而是一個HTMLCollection對象的時候。
HTMLCollection是由DOM方法返回的對象,好比:
document.getElementsByName()
document.getElementsByClassName()
document.getElementsByTagName()
還有不少其餘的HTMLCollection,這些對象是在DOM標準以前就已經在用了,這些HTMLCollection主要包括:
document.images
頁面中全部的IMG元素
document.links
頁面中全部的A元素
document.forms
頁面中全部的表單
document.forms[0].elements
頁面中第一個表單的全部字段
這些對象的問題在於,它們均是指向文檔(HTML頁面)中的活動對象。也就是說每次經過它們訪問集合的length時,老是會去查詢DOM,而DOM操做則是很耗資源的。
更好的辦法是爲for循環緩存住要遍歷的數組的長度,好比下面這段代碼:
for (var i = 0, max = myarray.length; i < max; i++) {
// do something with myarray[i]
}
經過這種方法只須要訪問DOM節點一次以得到length,在整個循環過程當中就均可以使用它。
無論在什麼瀏覽器中,在遍歷HTMLCollection時緩存length均可以讓程序執行的更快,能夠提速兩倍(Safari3)到一百九十倍(IE7)不等。更多細節能夠參照Nicholas Zakas的《高性能JavaScript》,這本書也是由O’Reilly出版。
須要注意的是,當你在循環過程當中須要修改這個元素集合(好比增長DOM元素)時,你更但願更新length而不是更新常量。
遵守單var模式,你能夠將var提到循環的外部,好比:
function looper() {
var i = 0,
max,
myarray = [];
// …
for (i = 0, max = myarray.length; i < max; i++) {
// do something with myarray[i]
}
}
這種模式帶來的好處就是提升了代碼的一致性,由於你愈來愈依賴這種單var模式。缺點就是在重構代碼的時候不能直接複製粘貼一個循環體,好比,你正在將某個循環從一個函數拷貝至另一個函數中,必須確保i和max也拷貝至新函數裏,而且須要從舊函數中將這些沒用的變量刪除掉。
最後一個須要對循環作出調整的地方是將i++替換成爲下面二者之一:
i = i + 1
i += 1
JSLint提示你這樣作,是由於++和–實際上下降了代碼的可讀性,若是你以爲無所謂,能夠將JSLint的plusplus選項設爲false(默認爲true),本書所介紹的最後一個模式用到了: i += 1。
關於這種for模式還有兩種變化的形式,作了少許改進,緣由有二:
減小一個變量(沒有max)
減量循環至0,這種方式速度更快,由於和零比較要比和非零數字或數組長度比較要高效的多
第一種變化形式是:
var i, myarray = [];
for (i = myarray.length; i–;) {
// do something with myarray[i]
}
第二種變化形式用到了while循環:
var myarray = [],
i = myarray.length;
while (i–) {
// do something with myarray[i]
}
這些小改進只體如今性能上,此外,JSLint不推薦使用i–。
for-in 循環
for-in 循環用於對非數組對象做遍歷。經過for-in進行循環也被稱做「枚舉」。
從技術角度講,for-in循環一樣能夠用於數組(JavaScript中數組便是對象),但不推薦這樣作。當使用自定義函數擴充了數組對象時,這時更容易產生邏輯錯誤。另外,for-in循環中屬性的遍歷順序是不固定的,因此最好數組使用普通的for循環,對象使用for-in循環。
能夠使用對象的hasOwnProperty()方法將從原型鏈中繼承來的屬性過濾掉,這一點很是重要。看一下這段代碼:
// the object
var man = {
hands: 2,
legs: 2,
heads: 1
};
// somewhere else in the code
// a method was added to all objects
if (typeof Object.prototype.clone === 「undefined」) {
Object.prototype.clone = function () {};
}
在這段例子中,咱們定義了一個名叫man的對象直接量。在代碼中的某個地方(能夠是man定義以前也能夠是以後),給Object的原型中增長了一個方法clone()。原型鏈是實時的,這意味着全部的對象均可以訪問到這個新方法。要想在枚舉man的時候避免枚舉出clone()方法,則須要調用hasOwnProperty()來對原型屬性進行過濾。若是不作過濾,clone()也會被遍歷到,而這不是咱們所但願的:
// 1.
// for-in loop
for (var i in man) {
if (man.hasOwnProperty(i)) { // filter
console.log(i, 「:」, man[i]);
}
}
/*
result in the console
hands : 2
legs : 2
heads : 1
*/
// 2.
// antipattern:
// for-in loop without checking hasOwnProperty()
for (var i in man) {
console.log(i, 「:」, man[i]);
}
/*
result in the console
hands : 2
legs : 2
heads : 1
clone: function()
*/
另一種的寫法是經過Object.prototype直接調用hasOwnProperty()方法,像這樣:
for (var i in man) {
if (Object.prototype.hasOwnProperty.call(man, i)) { // filter
console.log(i, 「:」, man[i]);
}
}
這種作法的好處是,當man對象中從新定義了hasOwnProperty方法時,能夠避免調用時的命名衝突(譯註:明確指定調用的是Object.prototype上的方法而不是實例對象中的方法),這種作法一樣能夠避免冗長的屬性查找過程(譯註:這種查找過程可能是在原型鏈上進行查找),一直查找到Object中的方法,你能夠定義一個變量來「緩存」住它(譯註:這裏所指的是緩存住Object.prototype.hasOwnProperty):
var i,
hasOwn = Object.prototype.hasOwnProperty;
for (i in man) {
if (hasOwn.call(man, i)) { // filter
console.log(i, 「:」, man[i]);
}
}
嚴格說來,省略hasOwnProperty()並非一個錯誤。根據具體的任務以及你對代碼的自信程度,你能夠省略掉它以提升一些程序執行效率。但當你對當前要遍歷的對象不肯定的時候,添加hasOwnProperty()則更加保險些。
這裏提到一種格式上的變化寫法(這種寫法沒法經過JSLint檢查),這種寫法在for循環所在的行加入了if判斷條件,他的好處是能讓循環語句讀起來更完整和通順(「若是元素包含屬性X,則拿X作點什麼」):
// Warning: doesn’t pass JSLint
var i,
hasOwn = Object.prototype.hasOwnProperty;
for (i in man) if (hasOwn.call(man, i)) { // filter
console.log(i, 「:」, man[i]);
}
(不)擴充內置原型
咱們能夠擴充構造函數的prototype屬性,這是一種很是強大的特性,用來爲構造函數增長功能,但有時這個功能強大到超過咱們的掌控。
給內置構造函數好比Object()、Array()、和Function()擴充原型看起來很是誘人,但這種作法嚴重下降了代碼的可維護性,由於它讓你的代碼變得難以預測。對於那些基於你的代碼作開發的開發者來講,他們更但願使用原生的JavaScript方法來保持工做的連續性,而不是使用你所添加的方法(譯註:由於原生的方法更可靠,而你寫的方法可能會有bug)。
另外,若是將屬性添加至原型中,極可能致使在那些不使用hasOwnProperty()作檢測的循環中將原型上的屬性遍歷出來,這會形成混亂。
所以,不擴充內置對象的原型是最好的,你也能夠本身定義一個規則,僅當下列條件知足時作例外考慮:
將來的ECMAScript版本的JavaScirpt會將你實現的方法添加爲內置方法。好比,你能夠實現ECMAScript5定義的一些方法,一直等到瀏覽器升級至支持ES5。這樣,你只是提早定義了這些有用的方法。
若是你發現你自定義的方法已經不存在,要麼已經在代碼其餘地方實現了,要麼是瀏覽器的JavaScript引擎已經內置實現了。
你所作的擴充附帶充分的文檔說明,且和團隊其餘成員作了溝通。
若是你遇到這三種狀況之一,你能夠給內置原型添加自定義方法,寫法以下:
if (typeof Object.protoype.myMethod !== 「function」) {
Object.protoype.myMethod = function () {
// implementation…
};
}
switch 模式
你能夠經過下面這種模式的寫法來加強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做爲結束
避免連續執行多個case語句塊(當省略break時會發生),若是你堅持認爲連續執行多case語句塊是最好的方法,請務必補充文檔說明,對於其餘人來講,這種狀況看起來是錯誤的。
以default結束整個switch,以確保即使是在找不到匹配項時也會有正常的結果,
避免隱式類型轉換
在JavaScript的比較操做中會有一些隱式的數據類型轉換。好比諸如false == 0或」「==0之類的比較都返回true。
爲了不隱式類型轉換造對程序形成干擾,推薦使用===和!===運算符,它們較除了比較值還會比較類型。
var zero = 0;
if (zero === false) {
// not executing because zero is 0, not false
}
// antipattern
if (zero == false) {
// this block is executed…
}
另一種觀點認爲當==夠用的時候就沒必要多餘的使用===。好比,當你知道typeof的返回值是一個字符串,就沒必要使用全等運算符。但JSLint卻要求使用全等運算符,這固然會提升代碼風格的一致性,並減小了閱讀代碼時的思考(「這裏使用==是故意的仍是無心的?」)。
避免使用eval()
當你想使用eval()的時候,不要忘了那句話「eval()是魔鬼」。這個函數的參數是一個字符串,它能夠執行任意字符串。若是事先知道要執行的代碼是有問題的(在運行以前),則沒有理由使用eval()。若是須要在運行時動態生成執行代碼,每每都會有更佳的方式達到一樣的目的,而非必定要使用eval()。例如,訪問動態屬性時能夠使用方括號:
// antipattern
var property = 「name」;
alert(eval(「obj.」 + property));
// preferred
var property = 「name」;
alert(obj[property]);
eval()一樣有安全隱患,由於你須要運行一些容易被幹擾的代碼(好比運行一段來自於網絡的代碼)。在處理Ajax請求所返回的JSON數據時會常遇到這種狀況,使用eval()是一種反模式。這種狀況下最好使用瀏覽器的內置方法來解析JSON數據,以確保代碼的安全性和數據的合法性。若是瀏覽器不支持JSON.parse(),你能夠使用JSON.org所提供的庫。
記住,多數狀況下,給setInterval()、setTimeout()和Function()構造函數傳入字符串的情形和eval()相似,這種用法也是應當避免的,這一點很是重要,由於這些情形中JavaScript最終仍是會執行傳入的字符串參數:
// antipatterns
setTimeout(「myFunc()」, 1000);
setTimeout(「myFunc(1, 2, 3)」, 1000);
// preferred
setTimeout(myFunc, 1000);
setTimeout(function () {
myFunc(1, 2, 3);
}, 1000);
new Function()的用法和eval()很是相似,應當特別注意。這種構造函數的方式很強大,但每每被誤用。若是你不得不使用eval(),你能夠嘗試用new Function()來代替。這有一個潛在的好處,在new Function()中運行的代碼會在一個局部函數做用域內執行,所以源碼中全部用var定義的變量不會自動變成全局變量。還有一種方法能夠避免eval()中定義的變量轉換爲全局變量,便是將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()進行數字轉換
能夠使用parseInt()將字符串轉換爲數字。函數的第二個參數是轉換基數(譯註:「基數」指的是數字進制的方式),這個參數一般被省略。但當字符串以0爲前綴時轉換就會出錯,例如,在表單中輸入日期的一個字段。ECMAScript3中以0爲前綴的字符串會被看成八進制數處理(基數爲8)。但在ES5中不是這樣。爲了不轉換類型不一致而致使的意外結果,應當老是指定第二個參數:
var month = 「06」,
year = 「09」;
month = parseInt(month, 10);
year = parseInt(year, 10);
在這個例子中,若是省略掉parseInt的第二個參數,好比parseInt(year),返回值是0,由於「09」被認爲是八進制數(等價於parseInt(year,8)),並且09是非法的八進制數。
字符串轉換爲數字還有兩種方法:
+」08」 // result is 8
Number(「08」) // 8
這兩種方法要比parseInt()更快一些,由於顧名思義parseInt()是一種「解析」而不是簡單的「轉換」。但當你指望將「08 hello」這類字符串轉換爲數字,則必須使用parseInt(),其餘方法都會返回NaN。
編碼風格
確立並遵照編碼規範很是重要,這會讓你的代碼風格一致、可預測、可讀性更強。團隊新成員經過學習編碼規範能夠很快進入開發狀態、並寫出團隊其餘成員易於理解的代碼。
在開源社區和郵件組中關於編碼風格的爭論一直不斷(好比關於代碼縮進,用tab仍是空格?)。所以,若是你打算在團隊內推行某種編碼規範時,要作好應對各類反對意見的心理準備,並且要吸收各類意見,這對確立並一向遵照某種編碼規範是很是重要,而不是斤斤計較的糾結於編碼規範的細節。
縮進
代碼沒有縮進幾乎就不能讀了,而不一致的縮進更加糟糕,由於它看上去像是遵循了規範,真正讀起來卻磕磕絆絆。所以規範的使用縮進很是重要。
有些開發者喜歡使用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;
}
花括號
應當老是使用花括號,即便是在可省略花括號的時候也應當如此。從技術角度講,若是if或for中只有一個語句,花括號是能夠省略的,但最好仍是不要省略。這讓你的代碼更加工整一致並且易於更新。
假設有這樣一段代碼,for循環中只有一條語句,你能夠省略掉這裏的花括號,並且不會有語法錯誤:
// bad practice
for (var i = 0; i < 10; i += 1)
alert(i);
但若是過了一段時間,你給這個循環添加了另外一行代碼?
// bad practice
for (var i = 0; i < 10; i += 1)
alert(i);
alert(i + 」 is 」 + (i % 2 ? 「odd」 : 「even」));
第二個alert實際處於循環體以外,但這裏的縮進會迷惑你。長遠考慮最好仍是寫上花括號,即使是在只有一個語句的語句塊中也應如此:
// better
for (var i = 0; i < 10; i += 1) {
alert(i);
}
同理,if條件句也應當如此:
// bad
if (true)
alert(1);
else
alert(2);
// better
if (true) {
alert(1);
} else {
alert(2);
}
左花括號的位置
開發人員對於左大括號的位置有着不一樣的偏好,在同一行呢仍是在下一行?
if (true) {
alert(「It’s TRUE!」);
}
或者:
if (true)
{
alert(「It’s TRUE!」);
}
在這個例子中,看起來只是我的偏好問題。但有時候花括號位置的不一樣則會影響程序的執行。由於JavaScript會「自動插入分號」。JavaScript對行結束時的分號並沒有要求,它會自動將分號補全。所以,當函數return語句返回了一個對象直接量,而對象的左花括號和return不在同一行時,程序的執行就和預想的不一樣了:
// warning: unexpected return value
function func() {
return
{
name: 「Batman」
};
}
能夠看出程序做者的意圖是返回一個包含了name屬性的對象,但實際狀況不是這樣。由於return後會填補一個分號,函數的返回值就是undefined。這段代碼等價於:
// warning: unexpected return value
function func() {
return undefined;
// unreachable code follows…
{
name: 「Batman」
};
}
結論,老是使用花括號,並且老是將左花括號與上一條語句放在同一行:
function func() {
return {
name: 「Batman」
};
}
關於分號應當注意:和花括號同樣,應當老是使用分號,儘管在JavaScript解析代碼時會補全行末省略的分號。嚴格遵照這條規則,可讓代碼更加嚴謹,同時能夠避免前面例子中所出現的歧義。
空格
空格的使用一樣有助於改善代碼的可讀性和一致性。在寫英文句子的時候,在逗號和句號後面會使用間隔。在JavaScript中,你能夠按照一樣的邏輯在表達式(至關於逗號)和語句結束(相對於完成了某個「想法」)後面添加間隔。
適合使用空格的地方包括:
for循環中的分號以後,好比 for (var i = 0; i < 10; i += 1) {…}
for循環中初始化多個變量,好比 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 () {};
另外,咱們推薦在運算符和操做數之間添加空格。也就是說在+, -, *, =, <, >, <=, >=, ===, !==, &&, ||, +=符號先後都添加空格。
// generous and consistent spacing
// makes the code easier to read
// allowing it to 「breathe」
var d = 0,
a = b + 1;
if (a && b && c) {
d = a % c;
a += d;
}
// antipattern
// missing or inconsistent spaces
// make the code confusing
var d= 0,
a =b+1;
if (a&& b&&c) {
d=a %c;
a+= d;
}
最後,還應當注意,最好在花括號旁邊添加空格:
在函數、if-else語句、循環、對象直接量的左花括號以前補充空格({)
在右花括號和else和while之間補充空格
垂直空白的使用常常被咱們忽略,你能夠使用空行來將代碼單元分隔開,就像文學做品中使用段落做分隔同樣。
命名規範
另一種能夠提高你代碼的可預測性和可維護性的方法是採用命名規範。也就是說變量和函數的命名都遵守同種習慣。
下面是一些建議的命名規範,你能夠原樣採用,也能夠根據本身的喜愛做調整。一樣,遵循規範要比規範自己更加劇要。
構造器命名中的大小寫
JavaScript中沒有類,但有構造函數,能夠經過new來調用構造函數:
var adam = new Person();
因爲構造函數畢竟仍是函數,無論咱們將它用做構造器仍是函數,固然但願只經過函數名就可分辨出它是構造器仍是普通函數。
首字母大寫能夠提示你這是一個構造函數,而首字母小寫的函數通常只認爲它是普通的函數,不該該經過new來調用它:
function MyConstructor() {…}
function myFunction() {…}
下一章將介紹一些強制將函數用做構造器的編程模式,但遵照咱們所提到的命名規範會更好的幫助程序員閱讀源碼。
單詞分隔
當你的變量名或函數名中含有多個單詞時,單詞之間的分隔也應當遵循統一的約定。最多見的作法是「駝峯式」命名,單詞都是小寫,每一個單詞的首字母是大寫。
對於構造函數,能夠使用「大駝峯式」命名,好比MyConstructor(),對於函數和方法,能夠採用「小駝峯式」命名,好比myFunction(),calculateArea()和getFirstName()。
那麼對於那些不是函數的變量應當如何命名呢?變量名一般採用小駝峯式命名,還有一個不錯的作法是,變量全部字母都是小寫,單詞之間用下劃線分隔,好比,first_name,favorite_bands和old_company_name,這種方法能夠幫助你區分函數和其餘標識符——原始數據類型或對象。
ECMAScript的屬性和方法均使用Camel標記法,儘管多字的屬性名稱是罕見的(正則表達式對象的lastIndex和ignoreCase屬性)。
在ECMAScript中的屬性和方法均使用駝峯式命名,儘管包含多單詞的屬性名稱(正則表達式對象中的lastIndex和ignoreCase)並不常見。
其餘命名風格
有時開發人員使用命名規範來彌補或代替語言特性的不足。
好比,JavaScript中沒法定義常量(儘管有一些內置常量好比Number.MAX_VALUE),因此開發者都採用了這種命名習慣,對於那些程序運行週期內不會更改的變量使用全大寫字母來命名。好比:
// precious constants, please don’t touch
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()則是私有方法。儘管這兩個方法本質上和公有方法無異,但在方法名前加下劃線前綴就是爲了警告用戶不要直接使用這兩個私有方法,由於不能保證它們在下一個版本中還能正常工做。JSLint會對私有方法做檢查,除非設置了JSLint的nomen選項爲false。
下面介紹一些_private風格寫法的變種:
在名字尾部添加下劃下以代表私有,好比name_和getElements_()
使用一個下劃線前綴代表受保護的屬性_protected,用兩個下劃線前綴代表私有屬性__private
在Firefox中實現了一些非標準的內置屬性,這些屬性在開頭和結束都有兩個下劃線,好比proto和parent
書寫註釋
寫代碼就要寫註釋,即使你認爲你的代碼不會被別人讀到。當你對一個問題很是熟悉時,你會很快找到問題代碼,但當過了幾個星期後再來讀這段代碼,則須要絞盡腦汁的回想代碼的邏輯。
你沒必要對顯而易見的代碼做過多的註釋:每一個變量和每一行都做註釋。但你須要對全部的函數、他們的參數和返回值補充註釋,對於那些有趣的或怪異的算法和技術也應當配備註釋。對於閱讀你的代碼的其餘人來講,註釋就是一種提示,只要閱讀註釋、函數名以及參數,就算不讀代碼也能大概理解程序的邏輯。好比,這裏有五到六行代碼完成了某個功能,若是提供了一行描述這段代碼功能的註釋,讀程序的人就沒必要再去關注代碼的細節實現了。代碼註釋的寫法並無硬性規定,有些代碼片斷(好比正則表達式)的確須要比代碼自己還多的註釋。
因爲過期的註釋會帶來不少誤導,這比不寫註釋還糟糕。所以保持註釋時刻更新的習慣很是重要,儘管對不少人來講這很難作到。
在下一小節咱們會講到,註釋能夠自動生成文檔。
書寫API文檔
不少人都以爲寫文檔是一件枯燥且吃力不討好的事情,但實際狀況不是這樣。咱們能夠經過代碼註釋自動生成文檔,這樣就不用再去專門寫文檔了。不少人以爲這是一個不錯的點子,由於根據某些關鍵字和格式化的文檔自動生成可閱讀的參考手冊自己就是「某種編程」。
傳統的APIdoc誕生自Java世界,這個工具名叫「javadoc」,和Java SDK(軟件開發工具包)一塊兒提供。但這個創意迅速被其餘語言借鑑。JavaScript領域有兩個很是優秀的開源工具,它們是JSDoc Toolkit(http://code.google.com/p/jsdoc-toolkit/ )和YUIDoc(http://yuilibrary.com/projects/yuidoc )。
生成API文檔的過程包括:
以特定的格式來組織書寫源代碼
運行工具來對代碼和註釋進行解析
發佈工具運行的結果,一般是HTML頁面
你須要學習這種特殊的語法,包括十幾種標籤,寫法相似於:
/**
* @tag value
*/
好比這裏有一個函數reverse(),能夠對字符串進行反序操做。它的參數和返回值都是字符串。給它補充註釋以下:
/**
* Reverse a string
*
* @param {String} input String to reverse
* @return {String} The reversed string
*/
var reverse = function (input) {
// …
return output;
};
能夠看到,@param是用來講明輸入參數的標籤,@return是用來講明返回值的標籤,文檔生成工具最終會爲將這種帶註釋的源代碼解析成格式化好的HTML文檔。
一個例子:YUIDoc
YUIDoc最初的目的是爲YUI庫(Yahoo! User Interface)生成文檔,但也能夠應用於任何項目,爲了更充分的使用YUIDoc你須要學習它的註釋規範,好比模塊和類的寫法(固然在JavaScript中是沒有類的概念的)。
讓咱們看一個用YUIDoc生成文檔的完整例子。
圖2-1展現了最終生成的文檔的模樣,你能夠根據項目須要隨意定製HTML模板,讓生成的文檔更加友好和個性化。
這裏一樣提供了在線的demo,請參照 http://jspatterns.com/book/2/。
這個例子中全部的應用做爲一個模塊(myapp)放在一個文件裏(app.js),後續的章節會更詳細的介紹模塊,如今只需知道用能夠用一個YUIDoc的標籤來表示模塊便可。
圖2-1 YUIDoc生成的文檔
pic
app.js的開始部分:
/**
* My JavaScript application
*
* @module myapp
*/
而後定義了一個空對象做爲模塊的命名空間:
var MYAPP = {};
緊接着定義了一個包含兩個方法的對象math_stuff,這兩個方法分別是sum()和multi():
/**
* A math utility
* @namespace MYAPP
* @class math_stuff
*/
MYAPP.math_stuff = {
/**
* Sums two numbers
*
* @method sum
* @param {Number} a First number
* @param {Number} b The second number
* @return {Number} The sum of the two inputs
*/
sum: function (a, b) {
return a + b;
},
/** * Multiplies two numbers * * @method multi * @param {Number} a First number * @param {Number} b The second number * @return {Number} The two inputs multiplied */ multi: function (a, b) { return a * b; }
};
這樣就結束了第一個「類」的定義,注意粗體表示的標籤。
@namespace
指向你的對象的全局引用
@class
表明一個對象或構造函數的不恰當的稱謂(JavaScript中沒有類)
@method
定義對象的方法,並指定方法的名稱
@param
列出函數須要的參數,參數的類型放在一對花括號內,跟隨其後的是參數名和描述
@return
和@param相似,用以描述方法的返回值,能夠不帶名字
咱們用構造函數來實現第二個「類」,給這個類的原型添加一個方法,可以體會到YUIDoc採用了不一樣的方式來建立對象:
/**
* Constructs Person objects
* @class Person
* @constructor
* @namespace MYAPP
* @param {String} first First name
* @param {String} last Last name
*/
MYAPP.Person = function (first, last) {
/**
* Name of the person
* @property first_name
* @type String
*/
this.first_name = first;
/**
* Last (family) name of the person
* @property last_name
* @type String
*/
this.last_name = last;
};
/**
* Returns the name of the person object
*
* @method getName
* @return {String} The name of the person
*/
MYAPP.Person.prototype.getName = function () {
return this.first_name + ’ ’ + this.last_name;
};
在圖2-1中能夠看到生成的文檔中Person構造函數的生成結果,粗體的部分是:
@constructor 暗示了這個「類」實際上是一個構造函數
@prototype 和 @type 用來描述對象的屬性
YUIDoc工具是語言無關的,只解析註釋塊,而不是JavaScript代碼。它的缺點是必需要在註釋中指定屬性、參數和方法的名字,好比,@property first_name。好處是一旦你熟練掌握YUIDoc,就能夠用它對任何語言源碼進行註釋的文檔化。
編寫易讀的代碼
這種將APIDoc格式的代碼註釋解析成API參考文檔的作法看起來很偷懶,但還有另一個目的,經過代碼重審來提升代碼質量。
不少做者或編輯會告訴你「編輯很是重要」,甚至是寫一本好書或好文章最最重要的步驟。將想法落實在紙上造成草稿只是第一步,草稿給讀者提的信息每每重點不明晰、結構不合理、或不符合按部就班的閱讀習慣。
對於編程也是一樣的道理,當你坐下來解決一個問題的時候,這時的解決方案只是一種「草案」,儘管能正常工做,可是不是最優的方法呢?是否是可讀性好、易於理解、可維護佳或容易更新?當一段時間後再來review你的代碼,必定會發現不少須要改進的地方,須要從新組織代碼或刪掉多餘的內容等等。這實際上就是在「整理」你的代碼了,能夠很大程度提升你的代碼質量。但事情每每不是這樣,咱們經常承受着高強度的工做壓力,根本沒有時間來整理代碼,所以經過代碼註釋寫文檔實際上是不錯的機會。
每每在寫註釋文檔的時候,你會發現不少問題。你也會從新思考源代碼中不合理之處,好比,某個方法中的第三個參數比第二個參數更經常使用,第二個參數多數狀況下取值爲true,所以就須要對這個方法接口進行適當的改造和包裝。
寫出易讀的代碼(或API),是指別人能輕易讀懂程序的思路。因此你須要採用更好的思路來解決手頭的問題。
儘管咱們認爲「草稿」不甚完美,但至少也算「抱佛腳」的權宜之計,一眼看上去是有點「草」,不過也無所謂,特別是當你處理的是一個關鍵項目時(會有人命懸與此)。其實你應當扔掉你所給出的第一個解決方案,雖然它是能夠正常工做的,但畢竟是一個草率的方案,不是最佳方案。你給出的第二個方案會更加靠譜,由於這時你對問題的理解更加透徹。第二個方案不是簡單的複製粘貼以前的代碼,也不能投機取巧尋找某種捷徑。
相互評審
另一種能夠提升代碼質量的方法是組織相互評審。同行的評審很正式也很規範,即使是求助於特定的工具,也不失是一種開發生產線上值得提倡的步驟。但你可能以爲沒有時間去做代碼互審,不要緊,你可讓坐在你旁邊的同事讀一下你的代碼,或者和她(譯註:注意是「她」而不是「他」)一塊兒過一遍你的代碼。
一樣,當你在寫APIDoc或任何其餘文檔的時候,同行的評審能幫助你的產出物更加清晰,由於你寫的文檔是讓別人讀的,你必須確保別人能理解你所做的東西。
同行的評審是一種很是不錯的習慣,不只僅是由於它能讓代碼變得更好,更重要的,在評審的過程當中,評審人和代碼做者經過分享和討論,兩人都能取長補短、相互促進。
若是你的團隊只有你一個開發人員,找不出第二我的能給你做代碼評審,這也不要緊。你能夠經過將你的代碼片斷開源,或把有意思的代碼片斷貼在博客中,會有人對你的代碼感興趣的。
另一個很是好的習慣是使用版本管理工具(CVS,SVN或Git),一旦有人修改並提交了代碼,都會發郵件通知組內成員。雖然大部分郵件都進入了垃圾箱,但老是會碰巧有人在工做間隙看到你所提交的代碼,並對代碼作出一些評價。
生產環境中的代碼壓縮(Minify)
這裏所說的代碼壓縮(Minify)是指去除JavaScript代碼中的空格、註釋以及其餘沒必要要的部分,用以減小JavaScript文件的體積,下降網絡帶寬損耗。咱們一般使用相似YUICompressor(Yahoo!)或Closure Compiler(Google)的壓縮工具來爲網頁加載提速。對於生產環境(譯註:「生產環境」指的是項目上線後的正式環境)中的腳本是須要做壓縮的,壓縮後的文件體積能減小至原來的一半如下。
下面這段代碼是壓縮後的樣子(這段代碼是YUI2庫中的Event模塊):
YAHOO.util.CustomEvent=function(D,C,B,A){this.type=D;this.scope=C||window;this.silent
=B;this.signature=A||YAHOO.util.CustomEvent.LIST;this.subscribers=[];if(!this.silent)
{}var E=」_YUICEOnSubscribe」;if(D!==E){this.subscribeEvent=new
YAHOO.util.CustomEvent(E,this,true);}…
除了去除空格、空行和註釋以外,壓縮工具還能縮短命名的長度(前提是保證代碼的安全),好比這段代碼中的參數A、B、C、D。壓縮工具只會重命名局部變量,由於更改全局變量會破壞代碼的邏輯。這也是要儘可能使用局部變量的緣由。若是你使用的全局變量是對DOM節點的引用,並且程序中屢次用到,最好將它賦值給一個局部變量,這樣能提升查找速度,代碼也會運行的更快,此外還能提升壓縮比、加快下載速度(譯註:在服務器開啓Gzip的狀況下,對下載速度的影響幾乎能夠忽略不計)。
補充說明一下,Goolge Closure Compiler還會對全局變量進行壓縮(在「高級」模式中),這是很危險的,且對編程規範的要求很是苛刻。它的好處是壓縮比很是高。
對生產環境的腳本作壓縮是至關重要的步驟,它能提高頁面性能,你應當使用工具來完成壓縮。千萬不要試圖手寫「壓縮好的」代碼,你應當堅持使用語義化的變量命名,並保留足夠的空格、縮進和註釋。你寫的代碼是須要被人閱讀的,因此應當將注意力放在代碼可讀性和可維護性上,代碼壓縮的工做交給工具去完成。
運行 JSLint
在上一章咱們已經介紹了JSLint,這裏咱們介紹更多的使用場景。對你的代碼進行JSLint檢查是很是好的編程習慣,你應該相信這一點。
JSLint的檢查點都有哪些呢?它會對本章討論過的一些模式(單var模式、parseInt()的第二個參數、老是使用花括號)作檢查。JSLint還包括其餘方面的檢查:
不可達代碼
在使用變量以前須要聲明
不安全的UTF字符
使用void、with、和eval
沒法正確解析的正則表達式
JSLint是基於JavaScript實現的(它是能夠經過JSLint檢查的),它提供了在線工具,也能夠下載使用,能夠運行於不少種平臺的JavaScript解析器。你能夠將源碼下載後在本地運行,支持的環境包括WSH(Windows Scripting Host,Windows)、JSC(JavaScriptCore,MacOSX)或Rhino(Mozilla開發的JavaScript引擎)。
能夠將JSLint下載後和你的代碼編輯器配置在一塊兒,着是一個不錯的注意,這樣每次你保存代碼的時候都會自動執行代碼檢查(好比配置快捷鍵)。
小結
本章咱們講解了編寫可維護性代碼的含義,本章的討論很是重要,它不只關係着軟件項目的成功與否,還關係到參與項目的工程師的「精神健康」和「幸福指數」。隨後咱們討論了一些最佳實踐和模式,它們包括:
減小全局對象,最好每一個應用只有一個全局對象 函數都使用單var模式來定義,這樣能夠將全部的變量放在同一個地方聲明,同時能夠避免「聲明提早」給程序邏輯帶來的影響。 for循環、for-in循環、switch語句、「禁止使用eval()」、不要擴充內置原型 遵照統一的編碼規範(在任何須要的時候保持空格、縮進、花括號和分號)和命名約定(構造函數、普通函數和變量)。 本章還討論了其餘一些和代碼自己無關的實踐,這些實踐和編碼過程緊密相關,包括書寫註釋、生成API文檔,組織代碼評審、不要試圖去手動了「壓縮」(minify)代碼而犧牲代碼可讀性、堅持使用JSLint來對代碼作檢查。