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