# 第一章 概述
JavaScript是一門Web開發語言。起初只是用來操作網頁中為數不多的元素(比如圖片和表單域),但誰也沒想到這門語言的成長是如此迅速。除了適用于客戶端瀏覽器編程,如今JavaScript程序可以運行于越來越多的平臺之上。你可以用它來進行服務器端開發(使用.Net或Node.js)、桌面應用程序開發(運行于桌面操作系統)、以及應用程序擴展(Firefox插件或者Photoshop擴展)、移動終端應用和純命令行的批處理腳本。
JavaScript同樣是一門不尋常的語言。它沒有類,許多場景中它使用函數作為一等對象。起初,許多開發者認為這門語言存在很多缺陷,但最近幾年情況發生了微妙的變化。有意思的是,有一些老牌語言比如Java和PHP也已經開始添加諸如閉包和匿名函數等新特性,而閉包和匿名函數則是JavaScript程序員最愿意津津樂道的話題。
JavaScript十分靈活,可以用你所熟悉的其他任何編程語言的編程風格來寫JavaScript程序。但最好的方式還是擁抱它所帶來的變化、學習它所特有的編程模式。
## 模式
對 “模式”的廣義解釋是“反復發生的事件或對象的固定用法...可以用來作為重復使用的模板或模型”(http://en.wikipedia.org/wiki/Pattern)。
在軟件開發領域,模式是指常見問題的通用解決方案。模式不是簡單的代碼復制和粘貼,而是一種最佳實踐,一種高級抽象,是解決某一類問題的范本。
識別這些模式非常重要,因為:
- 這些模式提供了經過論證的最佳實踐,它可以幫助我們更好的編碼,避免重復制造車輪。
- 這些模式提供了高一層的抽象,某個時間段內大腦只能處理一定復雜度的邏輯,因此當你處理更繁瑣棘手的問題時,它會幫你理清頭緒,你才不會被低級的瑣事阻礙大腦思考,因為所有的細枝末節都可以被歸類和切分成不同的塊(模式)。
- 這些模式為開發者和團隊提供了溝通的渠道,團隊開發者之間往往是異地協作,不會有經常面對面的溝通機會。簡單的代碼編寫技巧和技術問題處理方式的約定(代碼注釋)使得開發者之間的交流更加通暢。例如,“函數立即執行”用大白話表述成“你寫好一個函數后,在函數的結束花括號的后面添加一對括號,這樣能在定義函數結束后馬上執行這個函數”(我的天)。
本書將著重討論下面這三種模式:
- 設計模式(Design patterns)
- 編碼模式(Coding patterns)
- 反模式(Antipatterns)
設計模式最初的定義是來自于“GoF”(四人組,94年版“設計模式”的四個作者)的一本書,這本書在1994年出版,書名全稱是“設計模式:可復用面向對象軟件基礎”。書中列舉了一些重要的設計模式,比如單體、工廠、裝飾者、觀察者等等。但適用于JavaScript的設計模式并不多,盡管設計模式是脫離某種語言而存在的,但通常會以某種語言做范例來講解設計模式,這些語言多是強類型語言,比如C++和Java。有時直接將其應用于弱類型的動態語言比如JavaScript又顯得捉襟見肘。通常這些設計模式都是基于語言的強類型特性以及類的繼承。而JavaScript則需要某種輕型的替代方案。本書在第七章將討論基于 JavaScript實現的一些設計模式。
編碼模式更有趣一些。它們是JavaScript特有的模式和最佳實踐,它利用了這門語言獨有的一些特性,比如對函數的靈活運用,JavaScript編碼模式是本書所要討論的重點內容。
本書中你會偶爾讀到一點關于“反模式”的內容,顧名思義,反模式具有某些負作用甚至破壞性,書中會順便一提。反模式并不是bug或代碼錯誤,它只是一種處理問題的對策,只是這種對策帶來的麻煩遠超過他們解決的問題。在示例代碼中我們會對反模式做明顯的標注。
<a name="a2"></a>
## JavaScript:概念
在正式的討論之前,應當先理清楚JavaScript中的一些重要的概念,這些概念在后續章節中會經常碰到,我們先來快速過一下。
<a name="a3"></a>
### 面向對象
JavaScript 是一門面向對象的編程語言,對于那些倉促學習JavaScript并很快丟掉它的開發者來說,這的確有點讓人感到意外。你所能接觸到的任何JavaScript代碼片段都可以作為對象。只有五類原始類型不是對象,它們是數字、字符串、布爾值、null和undefined,前三種類型都有與之對應的包裝對象(下一章會講到)。數字、字符串和布爾值可以輕易的轉換為對象類型,可以通過手動轉換,也可以利用JavaScript解析器進行自動轉換。
函數也是對象,也可以擁有屬性和方法。
在任何語言中,最簡單的操作莫過于定義變量。那么,在JavaScript中定義變量的時候,其實也在和對象打交道。首先,變量自動變為一個被稱作“活動對象”的內置對象的屬性(如果是全局變量的話,就變為全局對象的屬性)。第二,這個變量實際上也是“偽對象”,因為它有自己的屬性(屬性特性),用以表示變量是否可以被修改、刪除或在for-in循環中枚舉。這些特性并未在ECMAScript3中作規定,而ECMAScript5中提供了一組可以修改這些特性的方法。
那么,到底什么是對象?對象能作這么多事情,那它們一定非常特別。實際上,對象是及其簡單的。對象只是很多屬性的集合,一個名值對的列表(在其他語言中可能被稱作關聯數組),這些屬性也可以是函數(函數對象),這種函數我們稱為“方法”。
關于對象還需要了解,我們可以隨時隨地修改你創建的對象(當然,ECMAScript5中提供了可阻止這些修改的API)。得到一個對象后,你可以給他添加、刪除或更新成員。如果你關心私有成員和訪問控制,本書中我們也會講到相關的編程模式。
最后一個需要注意的是,對象有兩大類:
- 本地對象(Native):由ECMAScript標準規范定義的對象
- 宿主對象(Host):由宿主環境創建的對象(比如瀏覽器環境)
本地對象也可以被歸類為內置對象(比如Array,Date)或自定義對象(var o = {})。
宿主對象包含window和所有DOM對象。如果你想知道你是否在使用宿主對象,將你的代碼遷移到一個非瀏覽器環境中運行一下,如果正常工作,那么你的代碼只用到了本地對象。
<a name="a4"></a>
### 無類
在本書中的許多場合都會反復碰到這個概念。JavaScript中沒有類,對于其他語言的編程老手來說這個觀念非常新穎,需要反復的琢磨和重新學習才能理解JavaScript只能處理對象的觀念。
沒有類,你的代碼變得更小巧,因為你不必使用類去創建對象,看一下Java風格的對象創建:
```
// Java object creation
HelloOO hello_oo = new HelloOO();
```
為了創建一個簡單的對象,同樣一件事情卻重復做了三遍,這讓這段代碼看起來很“重”。而大多數情況下,我們只想讓我們的對象保持簡單。
在JavaScript中,你需要一個對象,就隨手創建一個空對象,然后開始給這個對象添加有趣的成員。你可以給它添加原始值、函數或其他對象作為這個對象屬性。“空”對象并不是真正的空,對象中存在一些內置的屬性,但并沒有“自有屬性”。在下一章里我們對此作詳細討論。
“GoF”的書中提到一條通用規則,“組合優于繼承”,也就是說,如果你手頭有創建這個對象所需的資源,更推薦直接將這些資源組裝成你所需的對象,而不推薦先作分類再創建鏈式父子繼承的方式來創建對象。在JavaScript中,這條規則非常容易遵守,因為JavaScript中沒有類,而且對象組裝無處不在。
<a name="a5"></a>
### 原型
JavaScript中的確有繼承,盡管這只是一種代碼重用的方式(本書有專門的一章來討論代碼重用)。繼承可以有多種方式,最常用的方式就是利用原型。原型(prototype)是一個普通的對象,你所創建的每一個函數會自動帶有prototype屬性,這個屬性指向一個空對象,這個空對象包含一個constructor屬性,它指向你新建的函數而不是內置的Object(),除此之外它和通過對象直接量或Object()構造函數創建的對象沒什么兩樣。你可以給它添加新的成員,這些成員可以被其他的對象繼承,并當作其他對象的自有屬性來使用。
我們會詳細討論JavaScript中的繼承,現在只要記住:原型是一個對象(不是類或者其他什么特別的東西),每個函數都有一個prototype屬性。
<a name="a6"></a>
### 運行環境
JavaScript程序需要一個運行環境。一個天然的運行環境就是瀏覽器,但這絕不是唯一的運行環境。本書所討論的編程模式更多的和JavaScript語言核心(ECMAScript)相關,因此這些編程模式是環境無關的。有兩個例外:
- 第八章,這一章專門講述瀏覽器相關的模式
- 其他一些展示模式的實際應用的例子
運行環境會提供自己的宿主對象,這些宿主對象并未在ECMAScript標準中定義,它們的行為也是不可預知的。
<a name="a7"></a>
## ECMAScript 5
JavaScript語言的核心部分(不包含DOM、BOM和外部宿主對象)是基于ECMAScript標準(簡稱為ES)來實現的。其中第三版是在1999年正式頒布的,目前大多數瀏覽器都實現了這個版本。第四版已經廢棄了。第三版頒布后十年,2009年十二月,第五版才正式頒布。
第五版增加了新的內置對象、方法和屬性,但最重要的增加內容是所謂的嚴格模式(strict mode),這個模式移除了某些語言特性,讓程序變得簡單且健壯。比如,with語句的使用已經爭論了很多年,如今,在ECMAScript5嚴格模式中使用with則會報錯,而在非嚴格模式中則是ok的。我們通過一個指令來激活嚴格模式,這個指令在舊版本的語言實現中被忽略。也就是說,嚴格模式是向下兼容的,因為在不支持嚴格模式的舊瀏覽器中也不會報錯。
對于每一個作用域(包括函數作用域、全局作用域或在eval()參數字符串的開始部分),你可以使用這種代碼來激活嚴格模式:
```
function my() {
"use strict";
// rest of the function...
}
```
這樣就激活了嚴格模式,函數的執行則會被限制在語言的嚴格子集的范圍內。對于舊瀏覽器來說,這句話只是一個沒有賦值給任何變量的字符串,因此不會報錯。
按照語言的發展計劃,未來將會只保留“嚴格模式”。因此,現在的ES5只是一個過渡版本,它鼓勵開發者使用嚴格模式,而非強制。
本書不會討論ES5新增特性相關的模式,因為在本書截稿時并沒有任何瀏覽器實現了ES5,但本書的示例代碼通過一些技巧鼓勵開發者向新標準轉變:
- 確保所提供的示例代碼在嚴格模式下不包錯
- 避免使用并明確指出棄用的構造函數相關的屬性和方法,比如arguments.callee
- 針對ES5中的內置模式比如Object.create(),在ES3中實現等價的模式
<a name="a8"></a>
## JSLint
JavaScript是一種解釋型語言,它沒有靜態編譯時的代碼檢查,所以很可能將帶有簡單類型錯誤的破碎的程序部署到線上,而且往往意識不到這些錯誤的存在。這時我們就需要JSLint的幫助。
JSLint(http://jslint.com )是一個JavaScript代碼質量檢測工具,它的作者是 Douglas Crockford,JSLint會對代碼作掃描,并針對潛在的問題報出警告。筆者強烈推薦你在執行代碼前先通過JSlint作檢查。作者給出了警告:這個工具可能“會讓你不爽”,但僅僅是在開始使用它的時候不爽一下而已。你會很快從你的錯誤中吸取教訓,并學習這些成為一名專業的JavaScript程序員應當必備的好習慣。讓你的代碼通過JSLint的檢查,這會讓你對自己的代碼更加有自信,因為你不用再去擔心代碼中某個不起眼的地方丟失了逗號或者有某種難以察覺的語法錯誤。
當開始下一章的學習時,你將發現JSLint會被多次提到。本書中除了講解反模式的示例代碼外(有清楚的注釋說明)、所有示例代碼均通過了JSLint的檢查(使用JSLint的默認設置)。
<a name="a9"></a>
## 控制臺工具
console對象在本書中非常常見。這個對象并不是語言的一部分,而是運行環境的一部分,目前大多數瀏覽器也都實現了這個對象。比如在Firefox中,它是通過Firebug擴展引入進來的。Firebug控制臺工具包含UI操作界面,可以讓你快速輸入并測試JavaScript代碼片段,同樣用它可以調試當前打開的頁面(圖1-1)。在這里強烈推薦使用它來輔助學習。在Webkit核心的瀏覽器(Safari和Chrome)也提供了類似的工具,可以監控頁面情況,IE從版本8開始也提供了開發者工具。
本書中大多數代碼都使用console對象來輸出結果,而沒有使用alert()或者刷新當前頁面。因為用這種方法輸出結果實在太簡單了。
圖 1-1 使用Firebug控制臺工具

我們經常使用log()方法,它將傳入的參數在控制臺輸出,有時會用到dir(),用以將傳入的對象屬性枚舉出來,這里是一個例子:
```
console.log("test", 1, {}, [1,2,3]);
console.dir({one: 1, two: {three: 3}});
```
當你在控制臺輸入內容時,則不必使用console.log()。為了避免混亂,有些代碼片段仍然使用console.log()作輸出,并假設所有的代碼片段都使用控制臺來作檢測:
```
window.name === window['name']; // true
```
這和下面這種用法意思一樣:
```
console.log(window.name === window['name']);
```
這段代碼在控制臺中輸出為true。