想寫出高效的javascript類別庫卻無從下手;
嘗試閱讀別人的類別庫,卻理解得似懂給懂;
打算好好鑽研js高階函數,但權威書上的內容太零散,
即使記住“用法”,但到要“用”的時候卻沒有想“法”。
也許你和我一樣,好像有一顧無形的力量約束著我們的計劃,讓我們一再認為知識面的局限性,致使我們原地踏步,難以向前跨越。
這段時間,各種作業、課程設計、實驗報告,壓力倍增。難得擠出一點點時間,絕不睡懶覺,整理總結往日所看的書,只為了可以離寫自己的類庫近一點。
本文參考自《javascript語言精粹》和《Effective JavaScript》。例子都被調試過,理解過後,我想把一些「深奧」的道理說得淺顯一點點。
1.變數作用域
作用域對程式設計師來說就像氧氣。它無所不在,甚至,你往往不會去想他。但當它被污染時(例如使用全局物件),你會感覺到窒息(例如應用回應變慢)。 javascript核心作用域規則很簡單,精心設計,而且很強大。有效地使用javascript需要掌握變數作用域的一些基本概念,並了解一些可能導致難以捉摸的、令人討厭的問題的極端情況。
1.1盡量少用全域變數
javascript很容易在全域命名空間中建立變數。創建全域變數毫不費力,因為它不需要任何形式的聲明,而且能被整個程式的所有程式碼自動存取。
對於我們這些初學者,遇到某些需求(例如,傳輸的資料被記錄下來、等待某時機某函數呼叫時使用;或者是某函數被經常使用)時,好不猶豫想到全局函數,甚至大一學到的C語言面向過程思想太根深蒂固,系統整齊地都是滿滿函數。定義全域變數會污染共享的公共命名空間,並可能導致意外的命名衝突。全域變數也不利於模組化,因為它會導致程式中獨立組件間的不必要耦合。嚴重地說,過多的全局(包括樣式表,直接定義div或a的樣式),整合到多人開發過稱將會成為災難性錯誤。這就是為什麼jQuery的所有程式碼都被包裹在一個立即執行的匿名表達式中——自呼叫匿名函數。當瀏覽器載入完jQuery檔案後,自呼叫匿名函數立即開始執行,初始化jQuery的各個模組,避免破壞和污染全域變數以至於影響到其他程式碼。
另外,你或許會認為,「先怎麼怎麼寫,日後再整理」比較方便,但優秀的程式設計師會不斷地留意程式的結構、持續地歸類相關的功能以及分離不相關的組件,並這些行為作為程式設計過稱的一部分。
由於全域命名空間是javascript程式中獨立的元件經行互動的唯一途徑,因此,利用全域命名控制項的情況是不可避免的。元件或程式庫不得不定義一些全域變數。以便程式中的其他部分使用。否則最好使用局部變數。
javascript的全域命名空間也被暴露在程式全域作用域中可以存取的全域對象,該物件作為this關鍵字的初始值。在web瀏覽器中,全域物件被綁定在全域window變數。這意味著你創建全域變數有兩種方法:在全域作用域內使用var聲明他,或將其加入到全域物件中。使用var聲明的好處是能清楚表達全域變數在程式範圍中的影響。
鑑於引用為綁定的全域變數會導致執行階段錯誤,因此,保存作用域清晰和簡潔會使程式碼的使用者更容易理解程式聲明了那些全域變數。
由於全域物件提供了全域環境的動態反應機制,所以可以使用它來查詢一個運行環境,檢測在這個平台下哪些特性可用。
eg.ES5引入了一個全域的JSON物件來讀寫JSON格式的資料。
如果你提供了JSON的實現,你當然可以簡單無條件地使用自己的實作。但是由宿主環境提供的內建實作幾乎更適合的,因為它們是用C語言寫入瀏覽器的。因為它們按照一定的標準對正確性和一致性進行了嚴格檢查,並且普遍來說比第三方實現提供更好的性能。
當初資料結構課程設計模擬串的基本操作,要求不能使用語言本身提供的方法。 javascript對數組的基本操作實現得很好,如果只是出於一般的學習需要,模擬語言本身提供的方法的想法很好,但是如果真正投入開發,無需考慮第一時間選擇使用javascript內置方法。
1.2避免使用with
with語句提供任何「便利」,讓你的應用變得不可靠和低效率。我們需要對單一物件依序呼叫一系列方法。使用with語句可以很方便地避免對物件的重複引用:
使用with語句從模組物件中」導入「(import)變數也是很誘人的。
事實上,javascript對待所有的變數都是相同的。 javascript從最內層的作用域開始向外尋找變數。 with語言對待一個物件猶如該物件代表一個變數作用域,因此,在with程式碼區塊的內部,變數尋找從搜尋給定的變數名稱的屬性開始。如果在這個物件中沒有找到該屬性,則繼續在外部作用域中搜尋。 with區塊中的每個外部變數的參考都隱含地假設在with物件(以及它的任何原型物件)中沒有同名的屬性。而在程式的其他地方創建或修改with物件或其原型物件不一定會遵循這樣的假設。 javascript引擎當然不會讀取局部程式碼來取得你使用了那些局部變數。 javascript作用域可被表示為高效率的內部資料結構,變數查找會非常快速。但由於with程式碼區塊需要搜尋物件的原型鏈來尋找with程式碼裡的所有變量,因此,其運行速度遠低於一般的程式碼區塊。
替代with語言,簡單的做法,是將物件綁定在一個簡短的變數名稱上。
其他情況下,最好的方法是將局部變數明確地綁定到相關的屬性上。
1.3熟練閉包
理解閉包有單一概念:
a)javascript允許你引用在目前函數以外定義的變數。
b)即使外部函數已經返回,當前函數仍然可以引用在外部函數所定義的變數
javascriptd的函數值包含了比呼叫它們時所執行所需的程式碼還要多的資訊。而且,javascript函數值還在內部儲存它們可能會引用的定義在其封閉作用域的變數。那些在其所涵蓋的作用域內追蹤變數的函數被稱為閉包。
make函數就是一個閉包,其程式碼引用了兩個外在變數:magicIngredient和filling。每當make函數被呼叫時,其程式碼都能引用這兩個變量,因為閉包儲存了這兩個變數。
函數可以引用在其作用域內的任何變量,包括參數和外部函數變數。我們可以利用這一點來編寫更通用的sandwichMaker函數。
閉包是javascript最優雅、最具表現力的特性之一,也是許多習慣用法的核心。
c)閉包可以更新外部變數的值。 事實上,閉包儲存的是外部變數的引用,而不是它們的值的副本。因此,對於任何具有存取這些外部變數的閉包,都可以進行更新。
此範例產生一個包含三個閉包的物件。這三個閉包是set,type和get屬性,它們都共用存取val變量,set閉包更新val的值。隨後呼叫get和type查看更新的結果。
1.4理解變數宣告提升
javascript支援此法作用域(對變數foo的引用會被綁定到宣告foo變數最近的作用域中),但不支援區塊層級作用域(變數定義的作用域並不是離其最近的封閉語句或程式碼區塊)。
不明白這個特性將會導致一些微妙的bug:
1.5 當心命名函數表達式笨拙的作用域
同一段函數程式碼也可以當作一個表達式,卻有截然不同的意義。匿名函數和命名函數表達式的官方區別在於後者會綁定到與其函數名稱相同的變數上,該變數作為該函數的局部變數。這可以用來寫遞歸函數表達式。
值得注意的是,變數find的作用域只在其自身函數中,不像函數聲明,命名函數表達式不能透過其內部的函數名稱在外部被引用。
程式看起來會產生null,但其實會產生一個新的物件。
因為命名函數變數作用域內繼承了Object.prototype.constructor(即Oject的建構子),就像with語句一樣,這個作用域會因Object.prototype的動態改變而受到影響。在系統中避免物件污染函數表達式作用域的辦法是避免任何時候在Object.prototype中加入屬性,以避免使用任何與標準Object.prototype屬性同名的局部變數。
在流行的javascript引擎中另一個缺點是將命名函數表達式的宣告提升。
有些javascript環境甚至把f和g這兩個函數當作不同的對象,從而導致不必要的記憶體分配。
1.6 當心局部塊函數宣告笨拙的作用域
javascript沒有區塊級作用域,所以內部函數f的作用域應該是整個test函數。一些javascript環境確實如此,但並不是所有javascript環境都這樣,javascript實現在嚴格模式下將這類函數報告為錯誤(具有局部塊函數聲明的處於嚴格模式下的程序將報告成一個語法錯誤),有助於檢測不可移植程式碼,為未來的標準版本在給局部區塊函數聲明給更明智和可以的語義。針對這種情況,可以考慮在test函數內宣告一局部變數指向全域函數f。