??自從 2000 年以來,Web 開發(fā)方面的種種規(guī)范半开、條例正在高速發(fā)展隔披。Web 開發(fā)過去曾是荒蕪地帶,里面東西還都湊合寂拆,而現(xiàn)在已經(jīng)演化成了完整的研究規(guī)范奢米,并建立了種種最佳實踐。
??隨著簡單的網(wǎng)站成長為更加復(fù)雜的 Web 應(yīng)用纠永,同時 Web 愛好者成為了有收入的專業(yè)人事鬓长,Web 開發(fā)的世界充滿了各種關(guān)于最新技術(shù)和開發(fā)方法的信息。
??尤其是 JavaScript尝江,它從大量的研究和推斷中獲益涉波。JavaScript 的最佳實踐分成若干類,并在開發(fā)過程的不同點上進行處理炭序。
1啤覆、可維護性
??在早期的網(wǎng)站中,JavaScript 主要是用于小特效或者是表單驗證惭聂。而今天的 Web 應(yīng)用則會有成千上萬行 JavaScript 代碼窗声,執(zhí)行各種復(fù)雜的過程。
??這種演化讓開發(fā)者必須得考慮到可維護性辜纲。除了秉承較傳統(tǒng)理念的軟件工程師外笨觅,還要雇傭 JavaScript 開發(fā)人員為公司創(chuàng)造價值,而他們并非緊緊按時交付產(chǎn)品耕腾,同時還要開發(fā)智力成果在之后不斷增加價值屋摇。
??編寫可維護的代碼很重要,因為大部分開發(fā)人員都花費大量時間維護他人代碼幽邓。很難從頭開始開發(fā)新代碼的炮温,很多情況下是以他人工作成果為基礎(chǔ)的。確保自己代碼的可維護性牵舵,以便其他開發(fā)人員在此基礎(chǔ)上更好的開展工作柒啤。
??注意可維護的代碼的概念并不是 JavaScript 特有的倦挂。這里的很多概念都可以廣泛應(yīng)用于各種編程語言,當(dāng)然也有某些特定于 JavaScript 的概念担巩。
1.1方援、什么是可維護的代碼
??可維護的代碼有一些特征。一般來說涛癌,如果說代碼是可維護的犯戏,它需要遵循以下特點。
- 可理解性 —— 其他人可以接收代碼并理解它的意圖和一般途徑拳话,而無需原開發(fā)人員的完整解釋先匪。
- 直觀性 —— 代碼中的東西一看就能明白,不管其操作過程多么復(fù)雜弃衍。
- 可適應(yīng)性 —— 代碼以一種數(shù)據(jù)上的變化不要求完全重寫的方法撰寫呀非。
- 可擴展性 —— 在代碼架構(gòu)上已經(jīng)考慮到在未來允許對核心功能進行擴展。
- 可調(diào)試性 —— 當(dāng)有地方出錯時镜盯,代碼可以給予你足夠的信息來盡可能直接地確定問題所在岸裙。
??對于專業(yè)人士而言,能寫出可維護的 JavaScript 代碼是非常重要的技能速缆。這正是周末改改網(wǎng)站的愛好者和真正理解自己作品的開發(fā)人員之間的區(qū)別降允。
1.2、代碼約定
??一種讓代碼變得可維護的簡單途徑是形成一套 JavaScript 代碼的書寫規(guī)定艺糜。絕大多數(shù)語言都開發(fā)出了各自的代碼約定拟糕,只要在網(wǎng)上一搜就能找到大量相關(guān)文檔。
??專業(yè)的組織為開發(fā)人員制訂了詳盡的代碼約定試圖讓代碼對任何人都可維護倦踢。
??杰出的開放源代碼項目有著嚴格的代碼約定要求送滞,這讓社區(qū)中的任何人都可以輕松地理解代碼是如何組織的。
??由于 JavaScript 的可適應(yīng)性辱挥,代碼約定對它也很重要犁嗅。由于和大多數(shù)面向?qū)ο笳Z言不同,JavaScript 并不強制開發(fā)人員將所有東西都定義為對象晤碘。語言可以支持各種編程風(fēng)格褂微,從傳統(tǒng)面向?qū)ο笫降铰暶魇降胶瘮?shù)式。只要快速瀏覽一下一些開源 JavaScript 庫园爷,就能發(fā)現(xiàn)好幾種創(chuàng)建對象宠蚂、定義方法和管理環(huán)境的途徑。
??以下小節(jié)將討論代碼約定的概論童社。對這些主題的解說非常重要求厕,雖然可能的解說方式會有區(qū)別,這取決于個人需求。
1. 可讀性
??要讓代碼可維護呀癣,首先它必須可讀美浦。可讀性與代碼作為文本文件的格式化方式有關(guān)项栏∑直妫可讀性的大部分內(nèi)容都是和代碼的縮進相關(guān)的。當(dāng)所有人都使用一樣的縮進方式時沼沈,整個項目中的代碼都會更加易于閱讀流酬。
??通常會使用若干空格而非制表符來進行縮進,這是因為制表符在不同的文本編輯器中顯示效果不同列另。
??一種不錯的芽腾、很常見的縮進大小為 4 個空格,當(dāng)然你也可以使用其他數(shù)量访递。
??可讀性的另一方面是注釋晦嵌。在大多數(shù)編程語言中同辣,對每個方法的注釋都視為一個可行的實踐拷姿。因為 JavaScript 可以在代碼的任何地方創(chuàng)建函數(shù),所以這點常常被忽略了旱函。
??然而正因為如此响巢,在 JavaScript 中為每個函數(shù)編寫文檔就更加重要了。一般而言棒妨,有如下一些地方需要進行注釋踪古。
- 函數(shù)和方法 —— 每個函數(shù)或方法都應(yīng)該包含一個注釋,描述其目的和用于完成任務(wù)所可能使用的算法券腔。陳述事先的假設(shè)也非常重要伏穆,如參數(shù)代表什么,函數(shù)是否有返回值(因為這不能從函數(shù)定義中推斷出來)纷纫。
- 大段代碼 —— 用于完成單個任務(wù)的多行代碼應(yīng)該在前面放一個描述任務(wù)的注釋枕扫。
- 復(fù)雜的算法 —— 如果使用了一種獨特的方式解決某個問題,則要在注釋中解釋你是如何做的辱魁。這不僅僅可以幫助其他瀏覽你代碼的人烟瞧,也能在下次你自己查閱代碼的時候幫助理解。
- Hack —— 因為存在瀏覽器差異染簇,JavaScript 代碼一般會包含一些 hack参滴。不要假設(shè)其他人在看代碼的時候能夠理解 hack 所要應(yīng)付的瀏覽器問題。如果因為某種瀏覽器無法使用普通的方法锻弓,所以你需要用一些不同的方法砾赔,那么請將這些信息放在注釋中。這樣可以減少出現(xiàn)這種情況的可能性:有人偶然看到你的 hack,然后“修正”了它过蹂,最后重新引入了你本來修正了的錯誤十绑。
??縮進和注釋可以帶來更可讀的代碼,在未來則更容易維護酷勺。
2. 變量和函數(shù)命名
??適當(dāng)給變量和函數(shù)起名字對于增加代碼可理解性和可維護性是非常重要的本橙。由于很多 JavaScript 開發(fā)人員最初都只是業(yè)余愛好者,所以有一種使用無意義名字的傾向脆诉,諸如給變量起"foo"甚亭、"bar"等名字,給函數(shù)起"doSomething"這樣是名字击胜。
??專業(yè) JavaScript 開發(fā)人員必須克服這些惡習(xí)以創(chuàng)建可維護的代碼亏狰。命名的一般規(guī)則如下所示:
- 變量名應(yīng)為名詞如 car 或 person。
- 函數(shù)名應(yīng)該以動詞開始偶摔,如 getName()暇唾。返回布爾類型值的函數(shù)一般以 is 開頭,如 isEnable()辰斋。
- 變量和函數(shù)都應(yīng)使用合乎邏輯的名字策州,不要擔(dān)心長度。長度問題可以通過后處理和壓縮來緩解宫仗。
??必須避免出現(xiàn)無法表示所包含的數(shù)據(jù)類型的無用變量名够挂。有了合適的命名,代碼閱讀起來就像講述故事一樣藕夫,更容易理解孽糖。
3. 變量類型透明
??由于在 JavaScript 中變量是松散類型的,很容易就忘記變量所應(yīng)包含的數(shù)據(jù)類型毅贮。合適的命名方式可以在一定程度上緩解這個問題办悟,但放到所有的情況下看,還不夠滩褥。
??有三種表示變量數(shù)據(jù)類型的方式病蛉。
??第一種方式是初始化。
??當(dāng)定義了一個變量铸题,它應(yīng)該被初始化為一個值铡恕,來暗示它將來應(yīng)該如何應(yīng)用。例如丢间,將來保存布爾類型值的變量應(yīng)該初始化為 true 或者 false探熔,將來保存數(shù)字的變量就應(yīng)該初始化為一個數(shù)字,如以下例子所示:
// 通過初始化指定變量類型
var found = false; // 布爾型
var count = -1; // 數(shù)字
var name = ''; // 字符串
var person = null; // 對象
??初始化為一個特定的數(shù)據(jù)類型可以很好的指明變量的類型烘挫。但缺點是它無法用于函數(shù)聲明中的函數(shù)參數(shù)诀艰。
??第二種方法是使用匈牙利標記法來指定變量類型柬甥。
??匈牙利標記法在變量名之前加上一個或多個字符來表示數(shù)據(jù)類型。這個標記法在腳本語言中很流行其垄,曾經(jīng)很長時間也是 JavaScript 所推崇的方式苛蒲。
??JavaScript 中最傳統(tǒng)的匈牙利標記法是用單個字符表示基本類型:"o" 代表對象,"s" 代表字符串绿满,"i" 代表整數(shù)臂外,"f" 代表浮點數(shù),"b" 代表布爾型喇颁。如下所示:
// 用于指定數(shù)據(jù)類型的匈牙利標記法
var bFound; // 布爾型
var iCount; // 整數(shù)
var sName; // 字符串
var oPerson; // 對象
??JavaScript 中用匈牙利標記法的好處是函數(shù)參數(shù)一樣可以使用漏健。但它的缺點是讓代碼某種程度上難以閱讀,阻礙了沒有用它時代碼的直觀性和句子式的特質(zhì)橘霎。因此蔫浆,匈牙利標記法失去了一些開發(fā)者的寵愛。
??最后一種指定變量類型的方式是使用類型注釋姐叁。
??類型注釋放在變量名右邊瓦盛,但是在初始化前面。這種方式是在變量旁邊放一段指定類型的注釋外潜,如下所示:
// 用于指定類型的類型注釋
var found /*:Boolean*/ = false;
var count /*:int*/ = 10;
var name /*:String*/ = "Nicholas";
var person /*:Object*/ = null;
??類型注釋維持了代碼的整體可讀性原环,同時注入了類型信息。類型注釋的缺點是你不能用多行注釋一次注釋大塊的代碼橡卤,因為類型注釋也是多行注釋扮念,兩者會沖突损搬,如下例所示所示:
// 以下代碼不能正確運行
/*
var found /*:Boolean*/ = false;
var count /*:int*/ = 10;
var name /*:String*/ = "Nicholas";
var person /*:Object*/ = null;
*/
??這里碧库,試圖通過多行注釋注釋所有變量。類型注釋與其相沖突巧勤,因為第一次出現(xiàn)的 /* (第二行)匹配了第一次出現(xiàn)的*/(第 3 行)嵌灰,這會造成一個語法錯誤。
??如果你想注釋掉這些使用類型注釋的代碼行颅悉,最好在每一行上使用單行注釋(很多編輯器可以幫你完成)沽瞭。
??這就是最常見的三種指定變量數(shù)據(jù)類型的方法。每種都有各自的優(yōu)勢和劣勢剩瓶,要自己在使用之前進行評估驹溃。最重要的是要確定哪種最適合你的項目并一致使用。
1.3延曙、松散耦合
??只要應(yīng)用的某個部分過分依賴于另一部分豌鹤,代碼就是耦合過緊,難于維護枝缔。
??典型的問題如:對象直接引用另一個對象布疙,并且當(dāng)修改其中一個的同時需要修改另外一個。緊密耦合的軟件難于維護并且需要經(jīng)常重寫。
??因為 Web 應(yīng)用所涉及的技術(shù)灵临,有多種情況會使它變得耦合過緊截型。必須小心這些情況,并盡可能維護弱耦合的代碼儒溉。
1. 解耦 HTML/JavaScript
??一種最常見的耦合類型是 HTML/JavaScript 耦合宦焦。在 Web 上,HTML 和 JavaScript 各自代表了解決方案中的不同層次:HTML 是數(shù)據(jù)顿涣,JavaScript 是行為赶诊。因為它們天生就需要交互,所以有多種不同的方法將這兩個技術(shù)關(guān)聯(lián)起來园骆。
??但是舔痪,有一些方法會將 HTML 和 JavaScript 過于緊密地耦合在一起。直接寫在 HTML 中的 JavaScript锌唾,使用包含內(nèi)聯(lián)代碼的<script>元素或者是使用 HTML 屬性來分配事件處理程序锄码,都是過于緊密的耦合。請看以下代碼晌涕。
<!-- 使用了 <script> 的緊密耦合的 HTML/JavaScript -->
<script type="text/javascript">
document.write("Hello world!");
</script>
<!-- 使用事件處理程序?qū)傩灾档木o密耦合的 HTML/JavaScript -->
<input type="button" value="Click Me" onclick="doSomething()" />
??雖然這些從技術(shù)上來說都是正確的滋捶,但是實踐中,它們將表示數(shù)據(jù)的 HTML 和定義行為的 JavaScript 緊密耦合在了一起余黎。理想情況是重窟,HTML 和 JavaScript 應(yīng)該完全分離,并通過外部文件和使用 DOM 附加行為來包含 JavaScript惧财。
??當(dāng) HTML 和 JavaScript 過于緊密的耦合在一起時巡扇,出現(xiàn) JavaScript 錯誤時就要先判斷錯誤是出現(xiàn)在 HTML 部分還是在 JavaScript 文件中。它還會引入和代碼是否可用的相關(guān)新問題垮衷。
??在這個例子中厅翔,可能在 doSomething() 函數(shù)可用之前谣妻,就已經(jīng)按下了按鈕棚壁,引發(fā)了一個JavaScript 錯誤杀捻。因為任何對按鈕行為的更改要同時觸及 HTML 和 JavaScript叽粹,因此影響了可維護性份企。而這些更改本該只在 JavaScript 中進行耍休。
??HTML 和 JavaScript 的緊密耦合也可以在相反的關(guān)系上成立:JavaScript 包含了 HTML蛤迎。這通常會出現(xiàn)在使用 innerHTML 來插入一段 HTML 文本到頁面上這種情況中舔清,如下面的例子所示:
// 將 HTML 緊密耦合到 JavaScript
function insertMessage(msg){
var container = document.getElementById("container");
container.innerHTML = '<div class="msg"><p class="post">" + msg + "</p>" +
"<p><em>Latest message above.</em></p></div>";
}
??一般來說徐许,你應(yīng)該避免在 JavaScript 中創(chuàng)建大量 HTML施蜜。再一次重申要保持層次的分離,這樣可以很容易的確定錯誤來源绊寻。
??當(dāng)使用上面這個例子的時候花墩,有一個頁面布局的問題悬秉,可能和動態(tài)創(chuàng)建的 HTML 沒有被正確格式化有關(guān)。不過冰蘑,要定位這個錯誤可能非常困難和泌,因為你可能一般先看頁面的源代碼來查找那段煩人的 HTML,但是卻沒能找到祠肥,因為它是動態(tài)生成的武氓。對數(shù)據(jù)或者布局的更改也會要求更改 JavaScript,這也表明了這兩個層次過于緊密地耦合了仇箱。
??HTML 呈現(xiàn)應(yīng)該盡可能與 JavaScript 保持分離县恕。
??當(dāng) JavaScript 用于插入數(shù)據(jù)時,盡量不要直接插入標記剂桥。一般可以在頁面中直接包含并隱藏標記忠烛,然后等到整個頁面渲染好之后,就可以用 JavaScript 顯示該標記权逗,而非生成它美尸。
??另一種方法是進行 Ajax 請求并獲取更多要顯示的 HTML,這個方法可以讓同樣的渲染層(PHP斟薇、JSP师坎、Ruby 等等)來輸出標記,而不是直接嵌在 JavaScript 中堪滨。
??將 HTML 和 JavaScript 解耦可以在調(diào)試過程中節(jié)省時間胯陋,更加容易確定錯誤的來源,也減輕維護的難度:更改行為只需要在 JavaScript 文件中進行袱箱,而更改標記則只要在渲染文件中遏乔。
2. 解耦 CSS/JavaScript
??另一個 Web 層則是 CSS,它主要負責(zé)頁面的顯示犯眠。JavaScript 和 CSS 也是非常緊密相關(guān)的:他們都是 HTML 之上的層次按灶,因此常常一起使用症革。
??但是筐咧,和 HTML 與 JavaScript 的情況一樣,CSS 和 JavaScript 也可能會過于緊密地耦合在一起噪矛。最常見的緊密耦合的例子是使用 JavaScript 來更改某些樣式量蕊,如下所示:
// CSS 對 JavaScript 的緊密耦合
element.style.color = "red";
element.style.backgroundColor = "blue";
??由于 CSS 負責(zé)頁面的顯示,當(dāng)顯示出現(xiàn)任何問題時都應(yīng)該只是查看 CSS 文件來解決艇挨。然而残炮,當(dāng)使用了 JavaScript 來更改某些樣式的時候,比如顏色缩滨,就出現(xiàn)了第二個可能已更改和必須檢查的地方势就。結(jié)果是 JavaScript 也在某種程度上負責(zé)了頁面的顯示泉瞻,并與 CSS 緊密耦合了。如果未來需要更改樣式表苞冯,CSS 和 JavaScript 文件可能都需要修改袖牙。這就給開發(fā)人員造成了維護上的噩夢。所以在這兩個層次之間必須有清晰的劃分舅锄。
??現(xiàn)代 Web 應(yīng)用常常要使用 JavaScript 來更改樣式鞭达,所以雖然不可能完全將 CSS 和 JavaScript 解耦,但是還是能讓耦合更松散的皇忿。這是通過動態(tài)更改樣式類而非特定樣式來實現(xiàn)的畴蹭,如下例所示:
// CSS 對 JavaScript 的松散耦合
element.className = "edit";
??通過只修改某個元素的 CSS 類,就可以讓大部分樣式信息嚴格保留在 CSS 中鳍烁。JavaScript 可以更改樣式類叨襟,但并不會直接影響到元素的樣式。只要應(yīng)用了正確的類幔荒,那么任何顯示問題都可以直接追溯到
CSS 而非 JavaScript芹啥。
??第二類緊密耦合僅會在 IE 中出現(xiàn)(但運行于標準模式下的 IE8 不會出現(xiàn)),它可以在 CSS 中通過表達式嵌入 JavaScript铺峭,如下例所示:
/* JavaScript 對 CSS 的緊密耦合 */
div {
width: expression(document.body.offsetWidth - 10 + "px");
}
??通常要避免使用表達式墓怀,因為它們不能跨瀏覽器兼容,還因為它們所引入的 JavaScript 和 CSS 之間的緊密耦合卫键。如果使用了表達式傀履,那么可能會在 CSS 中出現(xiàn) JavaScript 錯誤。由于 CSS 表達式而追蹤過 JavaScript 錯誤的開發(fā)人員莉炉,會告訴你在他們決定看一下 CSS 之前花了多長時間來查找錯誤钓账。
??再次提醒,好的層次劃分是非常重要的絮宁。顯示問題的唯一來源應(yīng)該是 CSS梆暮,行為問題的唯一來源應(yīng)該是 JavaScript。在這些層次之間保持松散耦合可以讓你的整個應(yīng)用更加易于維護绍昂。
3. 解耦應(yīng)用邏輯/事件處理程序
??每個 Web 應(yīng)用一般都有相當(dāng)多的事件處理程序啦粹,監(jiān)聽著無數(shù)不同的事件。然而窘游,很少有能仔細得將應(yīng)用邏輯從事件處理程序中分離的唠椭。請看以下例子:
function handleKeyPress(event){
event = EventUtil.getEvent(event);
if (event.keyCode == 13){
var target = EventUtil.getTarget(event);
var value = 5 * parseInt(target.value);
if (value > 10){
document.getElementById("error-msg").style.display = "block";
}
}
}
??這個事件處理程序除了包含了應(yīng)用邏輯,還進行了事件的處理忍饰。這種方式的問題有其雙重性贪嫂。
??首先,除了通過事件之外就再沒有方法執(zhí)行應(yīng)用邏輯艾蓝,這讓調(diào)試變得困難力崇。如果沒有發(fā)生預(yù)想的結(jié)果怎么辦斗塘?是不是表示事件處理程序沒有被調(diào)用還是指應(yīng)用邏輯失敗亮靴?其次逛拱,如果一個后續(xù)的事件引發(fā)同樣的應(yīng)用邏輯,那就必須復(fù)制功能代碼或者將代碼抽取到一個單獨的函數(shù)中台猴。無論何種方式朽合,都要作比實際所需更多的改動。
??較好的方法是將應(yīng)用邏輯和事件處理程序相分離饱狂,這樣兩者分別處理各自的東西曹步。一個事件處理程序應(yīng)該從事件對象中提取相關(guān)信息,并將這些信息傳送到處理應(yīng)用邏輯的某個方法中休讳。例如讲婚,前面的代
碼可以被重寫為:
function validateValue(value){
value = 5 * parseInt(value);
if (value > 10){
document.getElementById("error-msg").style.display = "block";
}
}
function handleKeyPress(event){
event = EventUtil.getEvent(event);
if (event.keyCode == 13){
var target = EventUtil.getTarget(event);
validateValue(target.value);
}
}
??改動過的代碼合理將應(yīng)用邏輯從事件處理程序中分離了出來。handleKeyPress() 函數(shù)確認是按下了 Enter 鍵(event.keyCode 為 13)俊柔,取得了事件的目標并將 value 屬性傳遞給 validateValue() 函數(shù)筹麸,這個函數(shù)包含了應(yīng)用邏輯。
??注意 validateValue() 中沒有任何東西會依賴于任何事件處理程序邏輯雏婶,它只是接收一個值物赶,并根據(jù)該值進行其他處理。
??從事件處理程序中分離應(yīng)用邏輯有幾個好處留晚。首先酵紫,可以讓你更容易更改觸發(fā)特定過程的事件。如果最開始由鼠標點擊事件觸發(fā)過程错维,但現(xiàn)在按鍵也要進行同樣處理奖地,這種更改就很容易。其次赋焕,可以在不附加到事件的情況下測試代碼参歹,使其更易創(chuàng)建單元測試或者是自動化應(yīng)用流程。
??以下是要牢記的應(yīng)用和業(yè)務(wù)邏輯之間松散耦合的幾條原則:
- 勿將 event 對象傳給其他方法隆判;只傳來自 event 對象中所需的數(shù)據(jù)犬庇;
- 任何可以在應(yīng)用層面的動作都應(yīng)該可以在不執(zhí)行任何事件處理程序的情況下進行;
- 任何事件處理程序都應(yīng)該處理事件蜜氨,然后將處理轉(zhuǎn)交給應(yīng)用邏輯械筛。
??牢記這幾條可以在任何代碼中都獲得極大的可維護性的改進,并且為進一步的測試和開發(fā)制造了很多可能飒炎。
1.4、編程實踐
??書寫可維護的 JavaScript 并不僅僅是關(guān)于如何格式化代碼笆豁;它還關(guān)系到代碼做什么的問題郎汪。在企業(yè)環(huán)境中創(chuàng)建的 Web 應(yīng)用往往同時由大量人員一同創(chuàng)作赤赊。這種情況下的目標是確保每個人所使用的瀏覽器環(huán)境都有一致和不變的規(guī)則。因此煞赢,最好堅持以下一些編程實踐抛计。
1. 尊重對象所有權(quán)
??JavaScript 的動態(tài)性質(zhì)使得幾乎任何東西在任何時間都可以修改。有人說在 JavaScript 沒有什么神圣的東西照筑,因為無法將某些東西標記為最終或恒定狀態(tài)吹截。
??這種狀況在 ECMAScript 5 中通過引入防篡改對象得以改變;不過凝危,默認情況下所有對象都是可以修改的波俄。在其他語言中,當(dāng)沒有實際的源代碼的時候蛾默,對象和類是不可變的懦铺。
??JavaScript 可以在任何時候修改任意對象,這樣就可以以不可預(yù)計的方式覆寫默認的行為支鸡。因為這門語言沒有強行的限制冬念,所以對于開發(fā)者來說,這是很重要的牧挣,也是必要的急前。
??也許在企業(yè)環(huán)境中最重要的編程實踐就是尊重對象所有權(quán),它的意思是你不能修改不屬于你的對象瀑构。簡單地說叔汁,如果你不負責(zé)創(chuàng)建或維護某個對象、它的對象或者它的方法检碗,那么你就不能對它們進行修改据块。更具體地說:
- 不要為實例或原型添加屬性;
- 不要為實例或原型添加方法折剃;
- 不要重定義已存在的方法另假。
??問題在于開發(fā)人員會假設(shè)瀏覽器環(huán)境按照某個特定方式運行,而對于多個人都用到的對象進行改動就會產(chǎn)生錯誤怕犁。
??如果某人期望叫做 stopEvent() 的函數(shù)能取消某個事件的默認行為边篮,但是你對其進行了更改,然后它完成了本來的任務(wù)奏甫,后來還追加了另外的事件處理程序戈轿,那肯定會出現(xiàn)問題了。其他開發(fā)人員會認為函數(shù)還是按照原來的方式執(zhí)行阵子,所以他們的用法會出錯并有可能造成危害思杯,因為他們并不知道有副作用。
??這些規(guī)則不僅僅適用于自定義類型和對象挠进,對于諸如 Object色乾、String誊册、document、window 等原生類型和對象也適用暖璧。此處潛在的問題可能更加危險案怯,因為瀏覽器提供者可能會在不做宣布或者是不可預(yù)期的情況下更改這些對象。
??著名的 Prototype JavaScript 庫就出現(xiàn)過這種例子:它為 document 對象實現(xiàn)了 getElementsByClassName() 方法澎办,返回一個 Array 的實例并增加了一個 each()方法嘲碱。John Resig 在他的博客上敘述了產(chǎn)生這個問題的一系列事件。他在帖子(http://ejohn.org/blog/getelementsbyclassname-pre-prototype-16/)中說局蚀,他發(fā)現(xiàn)當(dāng)瀏覽器開始內(nèi)部實現(xiàn) getElementsByClassName() 的時候就出現(xiàn)問題了麦锯,這個方法并不返回一個 Array 而是返回一個并不包含 each() 方法的 NodeList。使用 Prototype 庫的開發(fā)人員習(xí)慣于寫這樣的代碼:
document.getElementsByClassName("selected").each(Element.hide);
??雖然在沒有原生實現(xiàn) getElementsByClassName() 的瀏覽器中可以正常運行至会,但對于支持的了瀏覽器就會產(chǎn)生錯誤离咐,因為返回的值不同。你不能預(yù)測瀏覽器提供者在未來會怎樣更改原生對象奉件,所以不管用任何方式修改他們宵蛀,都可能會導(dǎo)致將來你的實現(xiàn)和他們的實現(xiàn)之間的沖突。
??所以县貌,最佳的方法便是永遠不修改不是由你所有的對象术陶。所謂擁有對象,就是說這個對象是你創(chuàng)建的煤痕,比如你自己創(chuàng)建的自定義類型或?qū)ο笞置媪课喙6?Array、document 這些顯然不是你的摆碉,它們在你的代碼執(zhí)行前就存在了塘匣。你依然可以通過以下方式為對象創(chuàng)建新的功能:
- 創(chuàng)建包含所需功能的新對象,并用它與相關(guān)對象進行交互巷帝;
- 創(chuàng)建自定義類型忌卤,繼承需要進行修改的類型。然后可以為自定義類型添加額外功能楞泼。
??現(xiàn)在很多 JavaScript 庫都贊同并遵守這條開發(fā)原理驰徊,這樣即使瀏覽器頻繁更改,庫本身也能繼續(xù)成長和適應(yīng)堕阔。
2. 避免全局量
??與尊重對象所有權(quán)密切相關(guān)的是盡可能避免全局變量和函數(shù)棍厂。這也關(guān)系到創(chuàng)建一個腳本執(zhí)行的一致的和可維護的環(huán)境。最多創(chuàng)建一個全局變量超陆,讓其他對象和函數(shù)存在其中牺弹。請看以下例子:
// 兩個全局量——避免!!
var name = "Nicholas";
function sayName(){
alert(name);
}
??這段代碼包含了兩個全局量:變量 name 和函數(shù) sayName()例驹。其實可以創(chuàng)建一個包含兩者的對象捐韩,如下例所示:
// 一個全局量——推薦
var MyApplication = {
name: "Nicholas",
sayName: function(){
alert(this.name);
}
};
??這段重寫的代碼引入了一個單一的全局對象 MyApplication退唠,name 和 sayName() 都附加到其上鹃锈。這樣做消除了一些存在于前一段代碼中的一些問題。首先瞧预,變量 name 覆蓋了 window.name 屬性屎债,可能會與其他功能產(chǎn)生沖突;其次垢油,它有助消除功能作用域之間的混淆盆驹。
??調(diào)用 MyApplication.sayName() 在邏輯上暗示了代碼的任何問題都可以通過檢查定義 MyApplication 的代碼來確定。
??單一的全局量的延伸便是命名空間的概念滩愁,由 YUI(Yahoo! User Interface)庫普及躯喇。命名空間包括創(chuàng)建一個用于放置功能的對象。在 YUI 的 2.x 版本中硝枉,有若干用于追加功能的命名空間廉丽。比如:
- YAHOO.util.Dom —— 處理 DOM 的方法;
- YAHOO.util.Event —— 與事件交互的方法妻味;
- YAHOO.lang —— 用于底層語言特性的方法正压。
??對于 YUI,單一的全局對象 YAHOO 作為一個容器责球,其中定義了其他對象焦履。用這種方式將功能組合在一起的對象,叫做命名空間雏逾。整個 YUI 庫便是構(gòu)建在這個概念上的嘉裤,讓它能夠在同一個頁面上與其他的 JavaScript 庫共存。
??命名空間很重要的一部分是確定每個人都同意使用的全局對象的名字栖博,并且盡可能唯一屑宠,讓其他人不太可能也使用這個名字。
??在大多數(shù)情況下笛匙,可以是開發(fā)代碼的公司的名字侨把,例如 YAHOO 或者 Wrox。你可以如下例所示開始創(chuàng)建命名空間來組合功能妹孙。
// 創(chuàng)建全局對象
var Wrox = {};
// 為 Professional JavaScript 創(chuàng)建命名空間
Wrox.ProJS = {};
// 將書中用到的對象附加上去
Wrox.ProJS.EventUtil = { ... };
Wrox.ProJS.CookieUtil = { ... };
??在這個例子中秋柄,Wrox是全局量,其他命名空間在此之上創(chuàng)建蠢正。如果本書所有代碼都放在 Wrox.ProJS 命名空間骇笔,那么其他作者也應(yīng)把自己的代碼添加到 Wrox 對象中。只要所有人都遵循這個規(guī)則,那么就不用擔(dān)心其他人也創(chuàng)建叫做 EventUtil 或者 CookieUtil 的對象笨触,因為它會存在于不同的命名空間中懦傍。請看以下例子:
// 為 Professional Ajax 創(chuàng)建命名空間
Wrox.ProAjax = {};
// 附加該書中所使用的其他對象
Wrox.ProAjax.EventUtil = { ... };
Wrox.ProAjax.CookieUtil = { ... };
// ProJS 還可以繼續(xù)分別訪問
Wrox.ProJS.EventUtil.addHandler( ... );
// 以及 ProAjax
Wrox.ProAjax.EventUtil.addHandler( ... );
??雖然命名空間會需要多寫一些代碼,但是對于可維護的目的而言是值得的芦劣。命名空間有助于確保代碼可以在同一個頁面上與其他代碼以無害的方式一起工作粗俱。
3.避免與 null 進行比較
??由于 JavaScript 不做任何自動的類型檢查,所有它就成了開發(fā)人員的責(zé)任虚吟。因此寸认,在 JavaScript 代碼中其實很少進行類型檢測。最常見的類型檢測就是查看某個值是否為 null串慰。但是偏塞,直接將值與 null 比較是使用過度的,并且常常由于不充分的類型檢查導(dǎo)致錯誤邦鲫【牡穑看以下例子:
function sortArray(values){
if (values != null){ // 避免!
values.sort(comparator);
}
}
??該函數(shù)的目的是根據(jù)給定的比較子對一個數(shù)組進行排序庆捺。為了函數(shù)能正確執(zhí)行古今,values 參數(shù)必需是數(shù)組,但這里的 if 語句僅僅檢查該 values 是否為 null疼燥。還有其他的值可以通過 if 語句沧卢,包括字符串、數(shù)字醉者,它們會導(dǎo)致函數(shù)拋出錯誤但狭。
??現(xiàn)實中,與 null 比較很少適合情況而被使用撬即。必須按照所期望的對值進行檢查立磁,而非按照不被期望的那些。
??例如剥槐,在前面的范例中唱歧,values 參數(shù)應(yīng)該是一個數(shù)組,那么就要檢查它是不是一個數(shù)組粒竖,而不是檢查它是否非 null颅崩。函數(shù)按照下面的方式修改會更加合適:
function sortArray(values){
if (values instanceof Array){ // 推薦
values.sort(comparator);
}
}
??該函數(shù)的這個版本可以阻止所有非法值,而且完全用不著 null蕊苗。
??這種驗證數(shù)組的技術(shù)在多框架的網(wǎng)頁中不一定正確工作沿后,因為每個框架都有其自己的全局對象,因此朽砰,也有自己的 Array 構(gòu)造函數(shù)尖滚。如果你是從一個框架將數(shù)組傳送到另一個框架喉刘,那么就要另外檢查是否存在 sort() 方法。
??如果看到了與 null 比較的代碼漆弄,嘗試使用以下技術(shù)替換:
- 如果值應(yīng)為一個引用類型睦裳,使用 instanceof 操作符檢查其構(gòu)造函數(shù);
- 如果值應(yīng)為一個基本類型撼唾,使用 typeof 檢查其類型廉邑;
- 如果是希望對象包含某個特定的方法名,則使用 typeof 操作符確保指定名字的方法存在于對象上券坞。
??代碼中的 null 比較越少鬓催,就越容易確定代碼的目的肺素,并消除不必要的錯誤恨锚。
4. 使用常量
??盡管 JavaScript 沒有常量的正式概念,但它還是很有用的倍靡。這種將數(shù)據(jù)從應(yīng)用邏輯分離出來的思想猴伶,可以在不冒引入錯誤的風(fēng)險的同時,就改變數(shù)據(jù)塌西。請看以下例子:
function validate(value){
if (!value){
alert("Invalid value!");
location.href = "/errors/invalid.php";
}
}
??在這個函數(shù)中有兩段數(shù)據(jù):要顯示給用戶的信息以及 URL他挎。顯示在用戶界面上的字符串應(yīng)該以允許進行語言國際化的方式抽取出來。URL 也應(yīng)被抽取出來捡需,因為它們有隨著應(yīng)用成長而改變的傾向办桨。
??基本上,有著可能由于這樣那樣原因會變化的這些數(shù)據(jù)站辉,那么都會需要找到函數(shù)并在其中修改代碼 呢撞。而每次修改應(yīng)用邏輯的代碼,都可能會引入錯誤饰剥∈庀迹可以通過將數(shù)據(jù)抽取出來變成單獨定義的常量的方式,將應(yīng)用邏輯與數(shù)據(jù)修改隔離開來汰蓉。請看以下例子:
var Constants = {
INVALID_VALUE_MSG: "Invalid value!",
INVALID_VALUE_URL: "/errors/invalid.php"
};
function validate(value){
if (!value){
alert(Constants.INVALID_VALUE_MSG);
location.href = Constants.INVALID_VALUE_URL;
}
}
??在這段重寫過的代碼中绷蹲,消息和 URL 都被定義于 Constants 對象中誓斥,然后函數(shù)引用這些值漂佩。這些設(shè)置允許數(shù)據(jù)在無須接觸使用它的函數(shù)的情況下進行變更。Constants 對象甚至可以完全在單獨的文件中進行定義吱窝,同時該文件可以由包含正確值的其他過程根據(jù)國際化設(shè)置來生成若厚。
??關(guān)鍵在于將數(shù)據(jù)和使用它的邏輯進行分離拦英。要注意的值的類型如下所示。
- 重復(fù)值——任何在多處用到的值都應(yīng)抽取為一個常量盹沈。這就限制了當(dāng)一個值變了而另一個沒變的時候會造成的錯誤龄章。這也包含了 CSS 類名吃谣。
- 用戶界面字符串 —— 任何用于顯示給用戶的字符串,都應(yīng)被抽取出來以方便國際化做裙。
- URLs ——在 Web 應(yīng)用中岗憋,資源位置很容易變更,所以推薦用一個公共地方存放所有的 URL锚贱。
- 任意可能會更改的值 —— 每當(dāng)你在用到字面量值的時候仔戈,你都要問一下自己這個值在未來是不是會變化。如果答案是“是”拧廊,那么這個值就應(yīng)該被提取出來作為一個常量监徘。
??對于企業(yè)級的 JavaScript 開發(fā)而言,使用常量是非常重要的技巧吧碾,因為它能讓代碼更容易維護凰盔,并且在數(shù)據(jù)更改的同時保護代碼。
2倦春、性能
??自從 JavaScript 誕生以來户敬,用這門語言編寫網(wǎng)頁的開發(fā)人員有了極大的增長。與此同時睁本,JavaScript 代碼的執(zhí)行效率也越來越受到關(guān)注尿庐。
??因為 JavaScript 最初是一個解釋型語言,執(zhí)行速度要比編譯型語言慢得多呢堰。Chrome 是第一款內(nèi)置優(yōu)化引擎抄瑟,將 JavaScript 編譯成本地代碼的瀏覽器。此后枉疼,主流瀏覽器紛紛效仿皮假,陸續(xù)實現(xiàn)了 JavaScript 的編譯執(zhí)行。
??即使到了編譯執(zhí)行 JavaScript 的新階段往衷,仍然會存在低效率的代碼钞翔。不過,還是有一些方式可以改進代碼的整體性能的席舍。
2.1布轿、注意作用域
??隨著作用域鏈中的作用域數(shù)量的增加,訪問當(dāng)前作用域以外的變量的時間也在增加来颤。訪問全局變量總是要比訪問局部變量慢汰扭,因為需要遍歷作用域鏈。只要能減少花費在作用域鏈上的時間福铅,就能增加腳本的整體性能萝毛。
1.避免全局查找
??可能優(yōu)化腳本性能最重要的就是注意全局查找。使用全局變量和函數(shù)肯定要比局部的開銷更大滑黔,因為要涉及作用域鏈上的查找笆包。請看以下函數(shù):
function updateUI(){
var imgs = document.getElementsByTagName("img");
for (var i=0, len=imgs.length; i < len; i++){
imgs[i].title = document.title + " image " + i;
}
var msg = document.getElementById("msg");
msg.innerHTML = "Update complete.";
}
??該函數(shù)可能看上去完全正常环揽,但是它包含了三個對于全局 document 對象的引用。如果在頁面上有多個圖片庵佣,那么 for 循環(huán)中的 document 引用就會被執(zhí)行多次甚至上百次歉胶,每次都會要進行作用域鏈查找。
??通過創(chuàng)建一個指向 document 對象的局部變量巴粪,就可以通過限制一次全局查找來改進這個函數(shù)的性能:
function updateUI(){
var doc = document;
var imgs = doc.getElementsByTagName("img");
for (var i=0, len=imgs.length; i < len; i++){
imgs[i].title = doc.title + " image " + i;
}
var msg = doc.getElementById("msg");
msg.innerHTML = "Update complete.";
}
??這里通今,首先將 document 對象存在本地的 doc 變量中;然后在余下的代碼中替換原來的 document肛根。與原來的的版本相比辫塌,現(xiàn)在的函數(shù)只有一次全局查找,肯定更快派哲。
??將在一個函數(shù)中會用到多次的全局對象存儲為局部變量總是沒錯的臼氨。
2. 避免 with 語句
??在性能非常重要的地方必須避免使用 with 語句。和函數(shù)類似狮辽,with 語句會創(chuàng)建自己的作用域一也,因此會增加其中執(zhí)行的代碼的作用域鏈的長度。由于額外的作用域鏈查找喉脖,在 with 語句中執(zhí)行的代碼肯定會比外面執(zhí)行的代碼要慢。
??必須使用 with 語句的情況很少抑月,因為它主要用于消除額外的字符树叽。在大多數(shù)情況下,可以用局部變量完成相同的事情而不引入新的作用域谦絮。下面是一個例子:
function updateBody(){
with(document.body){
alert(tagName);
innerHTML = "Hello world!";
}
}
??這段代碼中的 with 語句讓 document.body 變得更容易使用题诵。其實可以使用局部變量達到相同的效果,如下所示:
function updateBody(){
var body = document.body
alert(body.tagName);
body.innerHTML = "Hello world!";
}
??雖然代碼稍微長了點层皱,但是閱讀起來比 with 語句版本更好性锭,它確保讓你知道 tagName 和 innerHTML 是屬于哪個對象的。同時叫胖,這段代碼通過將 document.body 存儲在局部變量中省去了額外的全局查找草冈。
2.2、選擇正確方法
??和其他語言一樣瓮增,性能問題的一部分是和用于解決問題的算法或者方法有關(guān)的怎棱。老練的開發(fā)人員根據(jù)經(jīng)驗可以得知哪種方法可能獲得更好的性能。很多應(yīng)用在其他編程語言中的技術(shù)和方法也可以在 JavaScript 中使用绷跑。
1. 避免不必要的屬性查找
??在計算機科學(xué)中拳恋,算法的復(fù)雜度是使用 O 符號來表示的。最簡單砸捏、最快捷的算法是常數(shù)值即 O(1)谬运。之后隙赁,算法變得越來越復(fù)雜并花更長時間執(zhí)行。下面的表格列出了 JavaScript 中常見的算法類型梆暖。
標 記 | 名 稱 | 描 述 |
---|---|---|
????O(1) | ????常數(shù) | 不管有多少值鸳谜,執(zhí)行的時間都是恒定的。一般表示簡單值和存儲在變量中的值 |
O(log n) | 對數(shù) | 總的執(zhí)行時間和值的數(shù)量相關(guān)式廷,但是要完成算法并不一定要獲取每個值咐扭。例如:二分查找 |
O(n) | 線性 | 總執(zhí)行時間和值的數(shù)量直接相關(guān)。例如:遍歷某個數(shù)組中的所有元素 |
O(n2) | 平方 | 總執(zhí)行時間和值的數(shù)量有關(guān)滑废,每個值至少要獲取n次蝗肪。例如:插入排序 |
??常數(shù)值,即 O(1)蠕趁,指代字面值和存儲在變量中的值薛闪。符號 O(1) 表示無論有多少個值,需要獲取常量值的時間都一樣俺陋。獲取常量值是非常高效的過程豁延。請看下面代碼:
var value = 5;
var sum = 10 + value;
alert(sum);
??該代碼進行了四次常量值查找:數(shù)字 5,變量 value腊状,數(shù)字 10 和變量 sum诱咏。這段代碼的整體復(fù)雜度被認為是 O(1)。
??在 JavaScript 中訪問數(shù)組元素也是一個 O(1) 操作缴挖,和簡單的變量查找效率一樣袋狞。所以以下代碼和前面的例子效率一樣:
var values = [5, 10];
var sum = values[0] + values[1];
alert(sum);
??使用變量和數(shù)組要比訪問對象上的屬性更有效率,后者是一個 O(n)操作映屋。對象上的任何屬性查找都要比訪問變量或者數(shù)組花費更長時間苟鸯,因為必須在原型鏈中對擁有該名稱的屬性進行一次搜索。簡而言
之棚点,屬性查找越多早处,執(zhí)行時間就越長。請看以下內(nèi)容:
var values = { first: 5, second: 10};
var sum = values.first + values.second;
alert(sum);
??這段代碼使用兩次屬性查找來計算 sum 的值瘫析。進行一兩次屬性查找并不會導(dǎo)致顯著的性能問題砌梆,但是進行成百上千次則肯定會減慢執(zhí)行速度。
??注意獲取單個值的多重屬性查找颁股。例如么库,請看以下代碼:
var query = window.location.href.substring(window.location.href.indexOf("?"));
??在這段代碼中,有 6 次屬性查找:window.location.href.substring() 有 3 次甘有,window.location.href.indexOf() 又有 3 次诉儒。只要數(shù)一數(shù)代碼中的點的數(shù)量,就可以確定屬性查找的次數(shù)了亏掀。
??這段代碼由于兩次用到了 window.location.href忱反,同樣的查找進行了兩次泛释,因此效率特別不好。
??一旦多次用到對象屬性温算,應(yīng)該將其存儲在局部變量中怜校。第一次訪問該值會是 O(n),然而后續(xù)的訪問都會是 O(1)注竿,就會節(jié)省很多茄茁。例如,之前的代碼可以如下重寫:
var url = window.location.href;
var query = url.substring(url.indexOf("?"));
??這個版本的代碼只有 4 次屬性查找巩割,相對于原始版本節(jié)省了 33%裙顽。在更大的腳本中進行這種優(yōu)化,傾向于獲得更多改進宣谈。
??一般來講愈犹,只要能減少算法的復(fù)雜度,就要盡可能減少闻丑。盡可能多地使用局部變量將屬性查找替換為值查找漩怎。進一步講,如果即可以用數(shù)字化的數(shù)組位置進行訪問嗦嗡,也可以使用命名屬性(諸如 NodeList
對象)勋锤,那么使用數(shù)字位置。
2. 優(yōu)化循環(huán)
??循環(huán)是編程中最常見的結(jié)構(gòu)酸钦,在 JavaScript 程序中同樣隨處可見怪得。優(yōu)化循環(huán)是性能優(yōu)化過程中很重要的一個部分,由于它們會反復(fù)運行同一段代碼卑硫,從而自動地增加執(zhí)行時間。
??在其他語言中對于循環(huán)優(yōu)化有大量研究蚕断,這些技術(shù)也可以應(yīng)用于 JavaScript欢伏。一個循環(huán)的基本優(yōu)化步驟如下所示。
- 減值迭代——大多數(shù)循環(huán)使用一個從 0 開始亿乳、增加到某個特定值的迭代器硝拧。在很多情況下,從最大值開始葛假,在循環(huán)中不斷減值的迭代器更加高效障陶。
- 簡化終止條件——由于每次循環(huán)過程都會計算終止條件,所以必須保證它盡可能快聊训。也就是說避免屬性查找或其他 O(n) 的操作抱究。
- 簡化循環(huán)體——循環(huán)體是執(zhí)行最多的,所以要確保其被最大限度地優(yōu)化带斑。確保沒有某些可以被很容易移出循環(huán)的密集計算鼓寺。
- 使用后測試循環(huán)——最常用 for 循環(huán)和 while 循環(huán)都是前測試循環(huán)勋拟。而如 do-while 這種后測試循環(huán),可以避免最初終止條件的計算妈候,因此運行更快敢靡。
??用一個例子來描述這種改動。以下是一個基本的 for 循環(huán):
for (var i=0; i < values.length; i++){
process(values[i]);
}
??這段代碼中變量 i 從 0 遞增到 values 數(shù)組中的元素總數(shù)苦银。假設(shè)值的處理順序無關(guān)緊要啸胧,那么循環(huán)可以改為 i 減值,如下所示:
for (var i=values.length -1; i >= 0; i--){
process(values[i]);
}
??這里幔虏,變量 i 每次循環(huán)之后都會減 1纺念。在這個過程中,將終止條件從 value.length 的 O(n) 調(diào)用簡化成了 0 的 O(1) 調(diào)用所计。由于循環(huán)體只有一個語句柠辞,無法進一步優(yōu)化。不過循環(huán)還能改成后測試循環(huán)主胧,如下:
var i=values.length -1;
if (i > -1){
do {
process(values[i]);
} while (--i >= 0);
}
??此處主要的優(yōu)化是將終止條件和自減操作符組合成了單個語句叭首。這時,任何進一步的優(yōu)化只能在 process() 函數(shù)中進行了踪栋,因為循環(huán)部分已經(jīng)優(yōu)化完全了焙格。
??記住使用“后測試”循環(huán)時必須確保要處理的值至少有一個∫亩迹空數(shù)組會導(dǎo)致多余的一次循環(huán)而“前測試”循環(huán)則可以避免眷唉。
3. 展開循環(huán)
??當(dāng)循環(huán)的次數(shù)是確定的,消除循環(huán)并使用多次函數(shù)調(diào)用往往更快 囤官。請看一下前面的例子冬阳。如果數(shù)組的長度總是一樣的,對每個元素都調(diào)用 process() 可能更優(yōu)党饮,如以下代碼所示:
// 消除循環(huán)
process(values[0]);
process(values[1]);
process(values[2]);
??這個例子假設(shè) values 數(shù)組里面只有 3 個元素肝陪,直接對每個元素調(diào)用 process()。這樣展開循環(huán)可以消除建立循環(huán)和處理終止條件的額外開銷刑顺,使代碼運行得更快氯窍。
??如果循環(huán)中的迭代次數(shù)不能事先確定,那可以考慮使用一種叫做 Duff 裝置的技術(shù)蹲堂。這個技術(shù)是以其創(chuàng)建者 Tom Duff 命名的狼讨,他最早在 C 語言中使用這項技術(shù)。正是 Jeff Greenberg 用 JavaScript 實現(xiàn)了
Duff 裝置柒竞。
??Duff 裝置的基本概念是通過計算迭代的次數(shù)是否為 8 的倍數(shù)將一個循環(huán)展開為一系列語句政供。
請看以下代碼:
//credit: Jeff Greenberg for JS implementation of Duff’s Device
// 假設(shè) values.length > 0
var iterations = Math.ceil(values.length / 8);
var startAt = values.length % 8;
var i = 0;
do {
switch(startAt){
case 0: process(values[i++]);
case 7: process(values[i++]);
case 6: process(values[i++]);
case 5: process(values[i++]);
case 4: process(values[i++]);
case 3: process(values[i++]);
case 2: process(values[i++]);
case 1: process(values[i++]);
}
startAt = 0;
} while (--iterations > 0);
??Duff 裝置的實現(xiàn)是通過將 values 數(shù)組中元素個數(shù)除以 8 來計算出循環(huán)需要進行多少次迭代的。然后使用取整的上限函數(shù)確保結(jié)果是整數(shù)。如果完全根據(jù)除 8 來進行迭代鲫骗,可能會有一些不能被處理到的元素犬耻,這個數(shù)量保存在 startAt 變量中。
??首次執(zhí)行該循環(huán)時执泰,會檢查 StartAt 變量看有需要多少額外調(diào)用枕磁。例如,如果數(shù)組中有 10 個值术吝,startAt 則等于 2计济,那么最開始的時候 process() 則只會被調(diào)用 2 次。在接下來的循環(huán)中排苍,startAt 被重置為 0沦寂,這樣之后的每次循環(huán)都會調(diào)用 8 次 process()。
??展開循環(huán)可以提升大數(shù)據(jù)集的處理速度淘衙。
??由 Andrew B. King 所著的 Speed Up Your Site(New Riders传藏,2003)提出了一個更快的 Duff 裝置技術(shù),將 do-while 循環(huán)分成 2 個單獨的循環(huán)彤守。以下是例子:
// credit: Speed Up Your Site (New Riders, 2003)
var iterations = Math.floor(values.length / 8);
var leftover = values.length % 8;
var i = 0;
if (leftover > 0){
do {
process(values[i++]);
} while (--leftover > 0);
}
do {
process(values[i++]);
process(values[i++]);
process(values[i++]);
process(values[i++]);
process(values[i++]);
process(values[i++]);
process(values[i++]);
process(values[i++]);
} while (--iterations > 0);
??在這個實現(xiàn)中毯侦,剩余的計算部分不會在實際循環(huán)中處理,而是在一個初始化循環(huán)中進行除以 8 的操作具垫。當(dāng)處理掉了額外的元素侈离,繼續(xù)執(zhí)行每次調(diào)用 8 次 process() 的主循環(huán)。這個方法幾乎比原始的 Duff 裝置實現(xiàn)快上 40%筝蚕。
??針對大數(shù)據(jù)集使用展開循環(huán)可以節(jié)省很多時間卦碾,但對于小數(shù)據(jù)集,額外的開銷則可能得不償失起宽。它是要花更多的代碼來完成同樣的任務(wù)洲胖,如果處理的不是大數(shù)據(jù)集,一般來說并不值得坯沪。
4. 避免雙重解釋
??當(dāng) JavaScript 代碼想解析 JavaScript 的時候就會存在雙重解釋懲罰宾濒。當(dāng)使用 eval() 函數(shù)或者是 Function 構(gòu)造函數(shù)以及使用 setTimeout() 傳一個字符串參數(shù)時都會發(fā)生這種情況。下面有一些例子:
// 某些代碼求值——避免!!
eval("alert('Hello world!')");
// 創(chuàng)建新函數(shù)——避免!!
var sayHi = new Function("alert('Hello world!')");
// 設(shè)置超時——避免!!
setTimeout("alert('Hello world!')", 500);
??在以上這些例子中屏箍,都要解析包含了 JavaScript 代碼的字符串。這個操作是不能在初始的解析過程中完成的橘忱,因為代碼是包含在字符串中的赴魁,也就是說在 JavaScript 代碼運行的同時必須新啟動一個解析器來解析新的代碼。
??實例化一個新的解析器有不容忽視的開銷钝诚,所以這種代碼要比直接解析慢得多颖御。
??對于這幾個例子都有另外的辦法。只有極少的情況下 eval() 是絕對必須的,所以盡可能避免使用潘拱。
??在這個例子中疹鳄,代碼其實可以直接內(nèi)嵌在原代碼中。對于 Function 構(gòu)造函數(shù)芦岂,完全可以直接寫成一般的函數(shù)瘪弓,調(diào)用 setTimeout() 可以傳入函數(shù)作為第一個參數(shù)。以下是一些例子:
// 已修正
alert('Hello world!');
// 創(chuàng)建新函數(shù)——已修正
var sayHi = function(){
alert('Hello world!');
};
// 設(shè)置一個超時——已修正
setTimeout(function(){
alert('Hello world!');
}, 500);
??如果要提高代碼性能禽最,盡可能避免出現(xiàn)需要按照 JavaScript 解釋的字符串腺怯。
5. 性能的其他注意事項
??當(dāng)評估腳本性能的時候,還有其他一些可以考慮的東西川无。下面并非主要的問題呛占,不過如果使用得當(dāng)也會有相當(dāng)大的提升。
- 原生方法較快——只要有可能懦趋,使用原生方法而不是自己用 JavaScript 重寫一個晾虑。原生方法是用諸如 C/C++之類的編譯型語言寫出來的,所以要比 JavaScript 的快很多很多仅叫。JavaScript 中最容易被忘記的就是可以在 Math 對象中找到的復(fù)雜的數(shù)學(xué)運算帜篇;這些方法要比任何用 JavaScript 寫
的同樣方法如正弦、余弦快的多惑芭。 - Switch 語句較快 —— 如果有一系列復(fù)雜的 if-else 語句坠狡,可以轉(zhuǎn)換成單個 switch 語句則可以得到更快的代碼。還可以通過將 case 語句按照最可能的到最不可能的順序進行組織遂跟,來進一步優(yōu)化 switch 語句逃沿。
- 位運算符較快 —— 當(dāng)進行數(shù)學(xué)運算的時候,位運算操作要比任何布爾運算或者算數(shù)運算快幻锁。選擇性地用位運算替換算數(shù)運算可以極大提升復(fù)雜計算的性能凯亮。諸如取模,邏輯與和邏輯或都可以考慮用位運算來替換哄尔。
2.3假消、最小化與語句數(shù)
??JavaScript 代碼中的語句數(shù)量也影響所執(zhí)行的操作的速度。完成多個操作的單個語句要比完成單個操作的多個語句快岭接。所以富拗,就要找出可以組合在一起的語句,以減少腳本整體的執(zhí)行時間鸣戴。這里有幾個可以參考的模式啃沪。
1. 多個變量聲明
??有個地方很多開發(fā)人員都容易創(chuàng)建很多語句,那就是多個變量的聲明窄锅。很容易看到代碼中由多個 var 語句來聲明多個變量创千,如下所示:
// 4 個語句——很浪費
var count = 5;
var color = "blue";
var values = [1,2,3];
var now = new Date();
??在強類型語言中,不同的數(shù)據(jù)類型的變量必須在不同的語句中聲明。然而追驴,在 JavaScript 中所有的變量都可以使用單個 var 語句來聲明械哟。前面的代碼可以如下重寫:
// 一個語句
var count = 5,
color = "blue",
values = [1,2,3],
now = new Date();
??此處,變量聲明只用了一個 var 語句殿雪,之間由逗號隔開暇咆。在大多數(shù)情況下這種優(yōu)化都非常容易做,并且要比單個變量分別聲明快很多冠摄。
2. 插入迭代值
??當(dāng)使用迭代值(也就是在不同的位置進行增加或減少的值)的時候糯崎,盡可能合并語句。請看以下代碼:
var name = values[i];
i++;
??前面這 2 句語句各只有一個目的:第一個從 values 數(shù)組中獲取值河泳,然后存儲在 name 中沃呢;第二個給變量 i 增加 1。這兩句可以通過迭代值插入第一個語句組合成一個語句拆挥,如下所示:
var name = values[i++];
??這一個語句可以完成和前面兩個語句一樣的事情薄霜。因為自增操作符是后綴操作符,i 的值只有在語句其他部分結(jié)束之后才會增加纸兔。一旦出現(xiàn)類似情況惰瓜,都要嘗試將迭代值插入到最后使用它的語句中去。
3. 使用數(shù)組和對象字面量
??你可能看過兩種創(chuàng)建數(shù)組和對象的方法:使用構(gòu)造函數(shù)或者是使用字面量汉矿。使用構(gòu)造函數(shù)總是要用到更多的語句來插入元素或者定義屬性崎坊,而字面量可以將這些操作在一個語句中完成。請看以下例子:
// 用 4 個語句創(chuàng)建和初始化數(shù)組——浪費
var values = new Array();
values[0] = 123;
values[1] = 456;
values[2] = 789;
// 用 4 個語句創(chuàng)建和初始化對象——浪費
var person = new Object();
person.name = "Nicholas";
person.age = 29;
person.sayName = function(){
alert(this.name);
};
??這段代碼中洲拇,只創(chuàng)建和初始化了一個數(shù)組和一個對象奈揍。各用了 4 個語句:一個調(diào)用構(gòu)造函數(shù),其他 3 個分配數(shù)據(jù)赋续。其實可以很容易地轉(zhuǎn)換成使用字面量的形式男翰,如下所示:
// 只用一條語句創(chuàng)建和初始化數(shù)組
var values = [123, 456, 789];
/ /只用一條語句創(chuàng)建和初始化對象
var person = {
name : "Nicholas",
age : 29,
sayName : function(){
alert(this.name);
}
};
??重寫后的代碼只包含兩條語句,一條創(chuàng)建和初始化數(shù)組纽乱,另一條創(chuàng)建和初始化對象蛾绎。之前用了八條語句的東西現(xiàn)在只用了兩條,減少了 75%的語句量鸦列。在包含成千上萬行 JavaScript 的代碼庫中租冠,這些優(yōu)
化的價值更大。
??只要有可能薯嗤,盡量使用數(shù)組和對象的字面量表達方式來消除不必要的語句肺稀。
??在 IE6 和更早版本中使用字面量有微小的性能懲罰。不過這些問題在 IE7 中已經(jīng)解決应民。
2.4、優(yōu)化 DOM 交互
??在 JavaScript 各個方面中,DOM 毫無疑問是最慢的一部分诲锹。DOM 操作與交互要消耗大量時間繁仁,因為它們往往需要重新渲染整個頁面或者某一部分。進一步說归园,看似細微的操作也可能要花很久來執(zhí)行黄虱,因為 DOM 要處理非常多的信息。
??理解如何優(yōu)化與 DOM 的交互可以極大得提高腳本完成的速度庸诱。
1. 最小化現(xiàn)場更新
??一旦你需要訪問的 DOM 部分是已經(jīng)顯示的頁面的一部分捻浦,那么你就是在進行一個現(xiàn)場更新。
??之所以叫現(xiàn)場更新桥爽,是因為需要立即(現(xiàn)場)對頁面對用戶的顯示進行更新朱灿。每一個更改,不管是插入單個字符钠四,還是移除整個片段盗扒,都有一個性能懲罰,因為瀏覽器要重新計算無數(shù)尺寸以進行更新∽喝ィ現(xiàn)場更新進行得越多侣灶,代碼完成執(zhí)行所花的時間就越長;完成一個操作所需的現(xiàn)場更新越少缕碎,代碼就越快褥影。請看以下例子:
var list = document.getElementById("myList"),
item,
i;
for (i=0; i < 10; i++) {
item = document.createElement("li");
list.appendChild(item);
item.appendChild(document.createTextNode("Item " + i));
}
??這段代碼為列表添加了 10 個項目。添加每個項目時咏雌,都有 2 個現(xiàn)場更新:一個添加<li>元素凡怎,另一個給它添加文本節(jié)點。這樣添加 10 個項目处嫌,這個操作總共要完成 20 個現(xiàn)場更新栅贴。
??要修正這個性能瓶頸,需要減少現(xiàn)場更新的數(shù)量熏迹。一般有 2 種方法檐薯。
??第一種是將列表從頁面上移除,最后進行更新注暗,最后再將列表插回到同樣的位置坛缕。這個方法不是非常理想,因為在每次頁面更新的時候它會不必要的閃爍捆昏。
??第二個方法是使用文檔片段來構(gòu)建 DOM 結(jié)構(gòu)赚楚,接著將其添加到 List 元素中。這個方式避免了現(xiàn)場更新和頁面閃爍問題骗卜。請看下面內(nèi)容:
var list = document.getElementById("myList"),
fragment = document.createDocumentFragment(),
item,
i;
for (i=0; i < 10; i++) {
item = document.createElement("li");
fragment.appendChild(item);
item.appendChild(document.createTextNode("Item " + i));
}
list.appendChild(fragment);
??在這個例子中只有一次現(xiàn)場更新宠页,它發(fā)生在所有項目都創(chuàng)建好之后左胞。文檔片段用作一個臨時的占位符,放置新創(chuàng)建的項目举户。然后使用 appendChild() 將所有項目添加到列表中烤宙。記住,當(dāng)給 appendChild() 傳入文檔片段時俭嘁,只有片段中的子節(jié)點被添加到目標躺枕,片段本身不會被添加的。
??一旦需要更新 DOM供填,請考慮使用文檔片段來構(gòu)建 DOM 結(jié)構(gòu)拐云,然后再將其添加到現(xiàn)存的文檔中。
2. 使用 innerHTML
??有兩種在頁面上創(chuàng)建 DOM 節(jié)點的方法:使用諸如 createElement() 和 appendChild() 之類的 DOM 方法近她,以及使用 innerHTML叉瘩。對于小的 DOM 更改而言,兩種方法效率都差不多泄私。然而房揭,對于大的 DOM 更改,使用 innerHTML 要比使用標準 DOM 方法創(chuàng)建同樣的 DOM 結(jié)構(gòu)快得多晌端。
??當(dāng)把 innerHTML 設(shè)置為某個值時捅暴,后臺會創(chuàng)建一個 HTML 解析器,然后使用內(nèi)部的 DOM 調(diào)用來創(chuàng)建 DOM 結(jié)構(gòu)咧纠,而非基于 JavaScript 的 DOM 調(diào)用蓬痒。由于內(nèi)部方法是編譯好的而非解釋執(zhí)行的,所以執(zhí)行快得多漆羔。前面的例子還可以用 innerHTML 改寫如下:
var list = document.getElementById("myList"),
html = "",
i;
for (i=0; i < 10; i++) {
html += "<li>Item " + i + "</li>";
}
list.innerHTML = html;
??這段代碼構(gòu)建了一個 HTML 字符串梧奢,然后將其指定到 list.innerHTML,便創(chuàng)建了需要的 DOM 結(jié)構(gòu)演痒。雖然字符串連接上總是有點性能損失亲轨,但這種方式還是要比進行多個 DOM 操作更快。
??使用 innerHTML 的關(guān)鍵在于(和其他 DOM 操作一樣)最小化調(diào)用它的次數(shù)鸟顺。例如惦蚊,下面的代碼在這個操作中用到 innerHTML 的次數(shù)太多了:
var list = document.getElementById("myList"),
i;
for (i=0; i < 10; i++) {
list.innerHTML += "<li>Item " + i + "</li>"; // 避免!!!
}
??這段代碼的問題在于每次循環(huán)都要調(diào)用 innerHTML,這是極其低效的讯嫂。調(diào)用 innerHTML 實際上就是一次現(xiàn)場更新蹦锋,所以也要如此對待。構(gòu)建好一個字符串然后一次性調(diào)用 innerHTML 要比調(diào)用 innerHTML 多次快得多欧芽。
3. 使用事件代理
??大多數(shù) Web 應(yīng)用在用戶交互上大量用到事件處理程序莉掂。頁面上的事件處理程序的數(shù)量和頁面響應(yīng)用戶交互的速度之間有個負相關(guān)。為了減輕這種懲罰千扔,最好使用事件代理憎妙。
??事件代理库正,用到了事件冒泡。任何可以冒泡的事件都不僅僅可以在事件目標上進行處理尚氛,目標的任何祖先節(jié)點上也能處理产园。使用這個知識戏阅,就可以將事件處理程序附加到更高層的地方負責(zé)多個目標的事件處理。如果可能组民,在文檔級別附加事件處理程序载迄,這樣可以處理整個頁面的事件讯柔。
4. 注意 HTMLCollection
??HTMLCollection 對象的陷阱對于 Web 應(yīng)用的性能而言是巨大的損害。記住护昧,任何時候要訪問 HTMLCollection魂迄,不管它是一個屬性還是一個方法,都是在文檔上進行一個查詢惋耙,這個查詢開銷很昂貴捣炬。
??最小化訪問 HTMLCollection 的次數(shù)可以極大地改進腳本的性能。
??也許優(yōu)化 HTMLCollection 訪問最重要的地方就是循環(huán)了绽榛。前面提到過將長度計算移入 for 循環(huán)的初始化部分∈幔現(xiàn)在看一下這個例子:
var images = document.getElementsByTagName("img"),
i, len;
for (i=0, len=images.length; i < len; i++){
// 處理
}
??這里的關(guān)鍵在于長度 length 存入了 len 變量,而不是每次都去訪問 HTMLCollection 的 length 屬性灭美。當(dāng)在循環(huán)中使用 HTMLCollection 的時候推溃,下一步應(yīng)該是獲取要使用的項目的引用,如下所示届腐,以便避免在循環(huán)體內(nèi)多次調(diào)用 HTMLCollection铁坎。
var images = document.getElementsByTagName("img"),
image,
i, len;
for (i=0, len=images.length; i < len; i++){
image = images[i];
// 處理
}
??這段代碼添加了 image 變量,保存了當(dāng)前的圖像犁苏。這之后硬萍,在循環(huán)內(nèi)就沒有理由再訪問 images 的 HTMLCollection 了 。
??編寫 JavaScript 的時候围详,一定要知道何時返回 HTMLCollection 對象朴乖,這樣你就可以最小化對他們的訪問。發(fā)生以下情況時會返回 HTMLCollection 對象:
- 進行了對 getElementsByTagName() 的調(diào)用短曾;
- 獲取了元素的 childNodes 屬性寒砖;
- 獲取了元素的 attributes 屬性;
- 訪問了特殊的集合嫉拐,如 document.forms哩都、document.images 等。
??要了解當(dāng)使用 HTMLCollection 對象時婉徘,合理使用會極大提升代碼執(zhí)行速度漠嵌。
3咐汞、部署
??也許所有 JavaScript 解決方案最重要的部分,便是最后部署到運營中的網(wǎng)站或者是 Web 應(yīng)用的過程儒鹿。
??在這之前可能你已經(jīng)做了相當(dāng)多的工作化撕,為普通的使用進行架構(gòu)并優(yōu)化一個解決方案。
??現(xiàn)在是時候從開發(fā)環(huán)境中走出來并進入 Web 階段了约炎,在此將會和真正的用戶交互植阴。然而,在這之前還有一系列需要解決的問題圾浅。
3.1掠手、構(gòu)建過程
??完備 JavaScript 代碼可以用于部署的一件很重要的事情,就是給它開發(fā)某些類型的構(gòu)建過程狸捕。軟件開發(fā)的典型模式是寫代碼—編譯—測試喷鸽,即首先書寫好代碼,將其編譯通過灸拍,然后運行并確保其正常工作做祝。
??由于 JavaScript 并非一個編譯型語言,模式變成了寫代碼—測試鸡岗,這里你寫的代碼就是你要在瀏覽器中測試的代碼混槐。這個方法的問題在于它不是最優(yōu)的,你寫的代碼不應(yīng)該原封不動地放入瀏覽器中纤房,理由如下所示纵隔。
- 知識產(chǎn)權(quán)問題 —— 如果把帶有完整注釋的代碼放到線上,那別人就更容易知道你的意圖炮姨,對它再利用捌刮,并且可能找到安全漏洞。
- 文件大小 —— 書寫代碼要保證容易閱讀舒岸,才能更好地維護绅作,但是這對于性能是不利的。瀏覽器并不能從額外的空白字符或者是冗長的函數(shù)名和變量名中獲得什么好處蛾派。
- 代碼組織 —— 組織代碼要考慮到可維護性并不一定是傳送給瀏覽器的最好方式俄认。
??基于這些原因,最好給 JavaScript 文件定義一個構(gòu)建過程洪乍。
??構(gòu)建過程始于在源控制中定義用于存儲文件的邏輯結(jié)構(gòu)眯杏。最好避免使用一個文件存放所有的 JavaScript,遵循以下面向?qū)ο笳Z言中的典型模式:將每個對象或自定義類型分別放入其單獨的文件中壳澳。
??這樣可以確保每個文件包含最少量的代碼岂贩,使其在不引入錯誤的情況下更容易修改。
??另外巷波,在使用像 CVS 或 Subversion 這類并發(fā)源控制系統(tǒng)的時候萎津,這樣做也減少了在合并操作中產(chǎn)生沖突的風(fēng)險卸伞。
??記住將代碼分離成多個文件只是為了提高可維護性,并非為了部署锉屈。要進行部署的時候荤傲,需要將這些源代碼合并為一個或幾個歸并文件。
??推薦 Web 應(yīng)用中盡可能使用最少的 JavaScript 文件颈渊,是因為 HTTP 請求是 Web 中的主要性能瓶頸之一遂黍。記住通過<script>標記引用 JavaScript 文件是一個阻塞操作,當(dāng)代碼下載并運行的時候會停止其他所有的下載俊嗽。
??因此妓湘,盡量從邏輯上將 JavaScript 代碼分組成部署文件。一旦組織好文件和目錄結(jié)構(gòu)乌询,并確定哪些要出現(xiàn)在部署文件中,就可以創(chuàng)建構(gòu)建系統(tǒng)了豌研。
3.2妹田、驗證
??盡管現(xiàn)在出現(xiàn)了一些可以理解并支持 JavaScript 的 IDE,大多數(shù)開發(fā)人員還是要在瀏覽器中運行代碼以檢查其語法鹃共。這種方法有一些問題鬼佣。
??首先,驗證過程難以自動化或者在不同系統(tǒng)間直接移植霜浴。其次晶衷,除了語法錯誤外,很多問題只有在執(zhí)行代碼的時候才會遇到阴孟,這給錯誤留下了空間晌纫;有些工具可以幫助確定 JavaScript 代碼中潛在的問題,例如 ESlint永丝。
??ESLint 可以查找 JavaScript 代碼中的語法錯誤以及常見的編碼錯誤锹漱。它可以發(fā)掘的一些潛在問題如下:
- eval() 的使用;
- 未聲明變量的使用慕嚷;
- 遺漏的分號哥牍;
- 不恰當(dāng)?shù)膿Q行;
- 錯誤的逗號使用喝检;
- 語句周圍遺漏的括號嗅辣;
- switch 分支語句中遺漏的 break;
- 重復(fù)聲明的變量挠说;
- with 的使用澡谭;
- 錯誤使用的等號(替代了雙等號或三等號);
- 無法到達的代碼纺涤。
??給開發(fā)周期添加代碼驗證這個環(huán)節(jié)有助于避免將來可能出現(xiàn)的一些錯誤译暂。建議開發(fā)人員給構(gòu)建過程加入某種類型的代碼驗證作為確定潛在問題的一個方法抠忘,防患于未然。
??JavaScript 代碼校驗工具的列表可以在附錄 D 中找到外永。
3.3崎脉、壓縮
??當(dāng)談及 JavaScript 文件壓縮,其實在討論兩個東西:代碼長度和配重(Wire weight)伯顶。
??代碼長度指的是瀏覽器所需解析的字節(jié)數(shù)囚灼,配重指的是實際從服務(wù)器傳送到瀏覽器的字節(jié)數(shù)。
??在 Web 開發(fā)的早期祭衩,這兩個數(shù)字幾乎是一樣的灶体,因為從服務(wù)器端到客戶端原封不動地傳遞了源文件。而在今天的 Web 上掐暮,這兩者很少相等蝎抽,實際上也不應(yīng)相等。
1. 文件壓縮
??因為 JavaScript 并非編譯為字節(jié)碼路克,而是按照源代碼傳送的樟结,代碼文件通常包含瀏覽器執(zhí)行所不需要的額外的信息和格式。注釋精算,額外的空白瓢宦,以及長長的變量名和函數(shù)名雖然提高了可讀性,但卻是傳送給瀏覽器時不必要的字節(jié)灰羽。不過驮履,我們可以使用壓縮工具減少文件的大小。
??壓縮器一般進行如下一些步驟:
- 刪除額外的空白(包括換行)廉嚼;
- 刪除所有注釋玫镐;
- 縮短變量名。
??JavaScript 有不少壓縮工具可用(附錄 D 中有一個完整列表)前鹅。
??所有的 JavaScript 文件在部署到生產(chǎn)環(huán)境之前摘悴,都應(yīng)該使用壓縮器或者類似的工具進行壓縮。
??給構(gòu)建過程添加一個壓縮 JavaScript 文件的環(huán)節(jié)以確保每次都進行這個操作舰绘。
2. HTTP 壓縮
??配重指的是實際從服務(wù)器傳送到瀏覽器的字節(jié)數(shù)蹂喻。因為現(xiàn)在的服務(wù)器和瀏覽器都有壓縮功能,這個字節(jié)數(shù)不一定和代碼長度一樣捂寿。
??所有的五大 Web 瀏覽器(IE口四、Firefox、Safari秦陋、Chrome 和 Opera)都支持對所接收的資源進行客戶端解壓縮蔓彩。這樣服務(wù)器端就可以使用服務(wù)器端相關(guān)功能來壓縮 JavaScript 文件。
??一個指定了文件使用了給定格式進行了壓縮的 HTTP 頭包含在了服務(wù)器響應(yīng)中。接著瀏覽器會查看該 HTTP 頭確定文件是否已被壓縮赤嚼,然后使用合適的格式進行解壓縮旷赖。結(jié)果是和原來的代碼量相比在網(wǎng)絡(luò)中傳遞的字節(jié)數(shù)量大大減少了。
??對于 Apache Web 服務(wù)器更卒,有兩個模塊可以進行 HTTP 壓縮:mod_gzip(Apache1.3.x)和 mod_deflate(Apache 2.0.x)等孵。
??對于 mod_gzip,可以給 httpd.conf 文件或者是.htaccess 文件添加以下代碼啟用對 JavaScript 的自動壓縮:
#告訴 mod_zip 要包含任何以.js 結(jié)尾的文件
mod_gzip_item_include file \.js$
??該行代碼告訴 mod_zip 要包含來自瀏覽器請求的任何以.js 結(jié)尾的文件蹂空。假設(shè)你所有的 JavaScript 文件都以.js 結(jié)尾俯萌,就可以壓縮所有請求并應(yīng)用合適的 HTTP 頭以表示內(nèi)容已被壓縮。
??對于 mod_deflate上枕,可以類似添加一行代碼以保證 JavaScript 文件在被發(fā)送之前已被壓縮咐熙。將以下這一行代碼添加到 httpd.conf 文件或者是.htaccess 文件中:
#告訴 mod_deflate 要包含所有的 JavaScript 文件
AddOutputFilterByType DEFLATE application/x-javascript
??注意這一行代碼用到了響應(yīng)的 MIME 類型來確定是否對其進行壓縮。記住雖然<script>的 type 屬性用的是 text/javascript辨萍,但是 JavaScript 文件一般還是用 application/x-javascript 作為其服務(wù)的 MIME 類型棋恼。
??mod_gzip 和 mod_deflate 都可以節(jié)省大約 70%的 JavaScript 文件大小。這很大程度上是因為 JavaScript 都是文本文件锈玉,因此可以非常有效地進行壓縮蘸泻。
??減少文件的配重可以減少需要傳輸?shù)綖g覽器的時間。記住有一點點細微的代價嘲玫,因為服務(wù)器必須花時間對每個請求壓縮文件,當(dāng)瀏覽器接收到這些文件后也需要花一些時間解壓縮并扇。不過去团,一般來說,這個代價還是值得的穷蛹。
??大部分 Web 服務(wù)器土陪,開源的或是商業(yè)的,都有一些 HTTP 壓縮功能肴熏。請查看服務(wù)器的文檔說明以確定如何合適地配置壓縮鬼雀。
小結(jié)
??隨著 JavaScript 開發(fā)的成熟,也出現(xiàn)了很多最佳實踐蛙吏。過去一度認為只是一種愛好的東西現(xiàn)在變成了正當(dāng)?shù)穆殬I(yè)源哩,同時還需要經(jīng)歷過去其他編程語言要做的一些研究,如可維護性鸦做、性能和部署励烦。
??JavaScript 中的可維護性部分涉及到下面的代碼約定。
- 來自其他語言中的代碼約定可以用于決定何時進行注釋泼诱,以及如何進行縮進坛掠,不過 JavaScript 需要針對其松散類型的性質(zhì)創(chuàng)造一些特殊的約定。
- 由于 JavaScript 必須與 HTML 和 CSS 共存,所以讓各自完全定義其自己的目的非常重要:JavaScript 應(yīng)該定義行為屉栓,HTML 應(yīng)該定義內(nèi)容舷蒲,CSS 應(yīng)該定義外觀。
- 這些職責(zé)的混淆會導(dǎo)致難以調(diào)試的錯誤和維護上的問題友多。
??隨著 Web 應(yīng)用中的 JavaScript 數(shù)量的增加牲平,性能變得更加重要,因此夷陋,你需要牢記以下事項欠拾。
- JavaScript 執(zhí)行所花費的時間直接影響到整個 Web 頁面的性能,所以其重要性是不能忽略的骗绕。
- 針對基于 C 的語言的很多性能的建議也適用于 JavaScript藐窄,如有關(guān)循環(huán)性能和使用 switch 語句替代 if 語句。
- 還有一個要記住的重要事情酬土,即 DOM 交互開銷很大荆忍,所以需要限制 DOM 操作的次數(shù)。
??流程的最后一步是部署撤缴。本章討論了以下一些關(guān)鍵點刹枉。
- 為了協(xié)助部署,推薦設(shè)置一個可以將 JavaScript 合并為較少文件(理想情況是一個)的構(gòu)建過程屈呕。
- 有了構(gòu)建過程也可以對源代碼自動運行額外的處理和過濾微宝。例如,你可以運行 JavaScript 驗證器來確保沒有語法錯誤或者是代碼沒有潛在的問題虎眨。
- 在部署前推薦使用壓縮器將文件盡可能變小蟋软。
- 和 HTTP 壓縮一起使用可以讓 JavaScript 文件盡可能小,因此對整體頁面性能的影響也會最小嗽桩。