醉心於Github上美妙的WebGL Demo之中,我試圖努力學習JavaScript。我曾經囫圇吞棗地將那本犀牛書《JavaScript權威指南》看了一遍,無奈功力不夠,對那些抽象的概念理解很有限。前一陣子亞馬遜有買300減100活動,湊書的時候忽然想起之前湯姆大叔送的那本《JavaScript編程精解》評價不錯,我當時手氣不好(其實主要還是沒有使用地精黑科技),沒有搶到,就在亞馬遜上買了一本。書拿到手后,發現其異乎尋常的薄,翻了兩頁感覺還不錯,於是決定花點時間以此書為基礎,再梳理一下JavaScript。
值和變量
值六種類型:number,string,Boolean,object,function,undefined,創建一個值的方法是使用直接量。值是由環境(瀏覽器)自動創建和維護的——當使用了值的直接量,環境就在內存中創建了這個值;當不再需要這個值的恰當的時候,這個值就被銷毀。除此之外,沒有可以影響或操縱值的方法。
變量是值的引用,通過關鍵字var聲明,由用戶負責創建和維護。變量可以“抓取”值,卻不能改變值:當為一個已賦值的變量賦新值的時候,實際上是創建了一個新值,並令該變量“抓取”之,而不是將新值填充到舊值中去。
var x = 1; var add = function(a,b){return a+b;} var origin = {x:0,y:0};
JavaScript中,數字是值,函數是值,對象也是值。上面三條語句的效果都是創建一個值,並聲明一個對象引用該值,本質上沒有區別。
函數:作用域鏈、閉包
JavaScript沒有塊級作用域,因此語句塊中的定義的變量在語句塊外是可見的。取而代之的是函數作用域,即在函數體內定義的變量在函數體外不可見,而函數體外定義的變量在函數體內可見。
var s = “hello”; var t = s+” world”; function test(){ var y = 1; t; //->“hello world” } y; //->undefined
函數本質上也是值(我會反復強調這一點,因為只有理解了這一點,才能明白JavaScript為什么是解釋型語言,為什么看起來和C++、C#相差那么大),在運行之前並不被環境所理解。瀏覽器像處理流一樣運行JavaScript文本,事實上它根本不知道下一條語句是什么,更不會嘗試“理解”該語言。因此JavaScript中的函數可以和定義函數的作用域中的其他變量直接交流,就像其他變量之間交流一樣。上面的代碼中,變量t和s有交流,函數變量test和t有交流,兩者沒有區別。在其他語言中,函數往往只通過參數和外界交流,而在JavaScript中函數可以和定義函數的作用域中的其他變量交流。有一點值得注意,就是函數中引用的函數外變量屬於定義函數時的作用域,而不是調用函數時的作用域,下面的代碼說明了這種令人費解的“閉包”特性——即,作用域並未隨着函數定義語句結束而消失,函數在調用的時候仍然能夠訪問到函數定義時的作用域。
function text(){ var s=”local string”; var f = function(){ return s; } return f; } var s = “global string”; text()();//->”local string”;
既然函數也是值,那么將函數本身作為參數傳入其他函數也很自然,對於只需要使用一次的函數僅使用匿名直接值也很自然。下面這樣的寫法很常見,也很方便,對於剛剛接觸JavaScript的同學可能需要習慣一段時間。
var theArray = [1,2,3,4]; var theSum = 0; function forEach(array, action){ if(array.length==undefined){ throw “Input is not array.” } for(var i=0;i<array.length;i++){ action(array[i]); } } forEach(theArray, function(n){ theSum+=n; })
上面的語句只有4句,瀏覽器對這4句語句順序執行:1.定義一個數組theArray;2.定義一個值theSum;3.定義一個函數forEach(函數是值,所以這一句和之前兩句沒有區別,都是將值賦給一個變量)。4.調用函數forEach,將theArray和一個匿名函數傳給forEach函數作為參數,該匿名函數影響了theSum值。
函數內部有隱式的變量arguments,該變量是一個數組,顧名思義表示函數傳入的參數。
JavaScript說到底是一種腳本語言,最初的使命就是在一個特定的環境下,完成一項特定的有始有終的工作。原則上完成某個工作,只需要順序編寫語句就可以了。函數對工作進行了抽象,因此對於復雜工作,往往先嵌套地定義函數,然后調用之。上面說到,這里的函數依賴於當前作用域,更類似於“過程”。這樣進行函數式的編程,最后程序的基本流程大致就變成了:定義全局變量、定義函數、調用函數這樣的形式。
對象和類:原型
JavaScript中的對象本質上就是名值對。對象也是是object類型的值,可以賦給變量。對象可以通過對象直接量創建,也可以通過構造函數創建。
var point1 = {x:1,y:0,z:0} var point2 = {x:0,y:1,z:0} function Point(theX,theY,theZ){ this.x=theX; this.y=theY; this.z=theZ; } var point3 = new point(1,1,0);
對象最重要的特征是繼承。在其他面向對象編程語言中,編譯器很輕松地知道,哪幾個對象時同一類的,哪個類繼承了哪個類,這在JavaScript中是由原型實現的。每一個對象都有一個原型對象,可以通過prototype字段訪問。對象的原型對象就是對象構造函數的原型,比如point3.prototype就是Point.prototype,構造函數的原型對象在定義構造函數時也就生成了。構造函數是值,因此即使是函數體完全相同的構造函數,其值也是不一樣的,其原型自然也就不一樣。具有相同原型的對象,往往由相同的構造函數產生,這意味着這些對象是“同一類”,如同C#中同一類的多個實例一樣。以對象直接量(即名值對,如point1和point2)賦值給對象的時候,實際上是隱式調用了構造函數Object(),因此這樣的對象其原型是Object.prototype。當對象訪問某個屬性(即通過名查詢值)的時候,首先查看對象本身是否是具有該值,然后是原型是否具有,然后是原型的原型是否具有……直到Object.prototype,只有該對象沒有原型。
具有相同原型的對象共享原型(這好像是廢話),使得原型成為儲存這些對象的公有成員的理想場所。比如,每個三角形對象不一樣,但是計算三角形面積的方法是一樣的,因此該方法應當定義在三角形對象的原型上,否則,對於每個三角形對象都會生成一個一模一樣的函數——函數是值,正如我們之前強調的一樣,函數會占用空間。
function Triangle(thePoint1, thePoint2, thePoint3){……} Triangle.prototype.area = function(){……}; var t=Triangle(point1, point2, point3); t.area(); //->0.5
上面的代碼使用了本節開始處代碼的上下文,實際上point1和point2是同一類,point3是另外一類,這並不影響它們在Triangle函數中的表現,只需要能夠提供x、y、z字段的對象都能正常工作。
錯誤處理
使用try…catch…finally塊作錯誤處理,使用throw關鍵字拋出異常。異常由用戶自己定義,如使用對象直接量,注意這里的type不是關鍵字,可以換成其他需要的字眼。
throw {
type:”ArgumentInvalidException”,
message:”Input is not number.”
}
往往希望在某一層次只處理某一種類的異常,非該種類異常交由上層處理,可以使用捕捉異常、檢查異常、不符合要求再拋出的方法。
catch(e){ if(e.type!=” ArgumentInvalidException”){ throw e; } // 處理ArgumentInvalidException類型的異常 }
對功能函數編寫測試函數,盡可能多地實驗各種異常情況是好的習慣。