什麼叫做類型?簡單地說,型別就是把記憶體中的一個二進位序列賦予某種意義。例如,二進位序列0100 0000 0111 0000 0001 0101 0100 1011 1100 0110 1010 0111 1110 1111 1001 1110如果整數看14646464位 IEEE 54規定的浮點數二進位表示規則(見附1)雙精度浮點類型則是257.331。
大部分電腦語言使用變數來儲存和表示資料,有些語言會給變數規定一個型別,在整個程式中(不論是編譯時還是執行時),這個型別都不能被改變。與此相對,JavaScript和一些其它語言的變數可以儲存任何類型,它們使用無類型的變數。變數類型是否存在,是跟語法無關的,例如C#中也提供了var類型的變量,但是,下面的語句在C#中會出錯:
var a=1;
a=”string”;
原因是C#的var關鍵字只是省略了變數類型聲明,而根據初始化表達式自動推斷變數類型,所以C#的var變數仍然是有類型的。而JavaScript中,任何時刻你都可以把任何值賦值給特定變量,所以JavaScript變數是無類型的。
依照電腦語言的型別系統的設計方式,可以分為強型別和弱型種兩種。二者之間的區別,就在於計算時是否可以不同類型之間對使用者透明地隱式轉換。從使用者的角度來看,如果一個語言可以隱式轉換它的所有類型,那麼它的變數、表達式等在參與運算時,即使類型不正確,也能透過隱式轉換來正確地類型,這對使用者而言,就好像所有類型都能進行所有運算一樣,所以這樣的語言被稱為弱類型。與此相對,強型別語言的型別之間不一定有隱式轉換(例如C++是強型別語言,但C++中double和int可以互相轉換,但double和任何型別的指標之間都需要強制轉換)
類型可以幫助程式設計師編寫正確的程序,它在實際編寫程式的過程中體現為一種約束。一般法則是,約束越強越不容易出錯,但寫程式時也越麻煩。變數有類型的強型別語言約束最強,典型代表是C++,變數無型別的弱型別語言約束最弱,典型代表是JavaScript。在JavaScript中,因為約束比較弱,所以容易發生這種錯誤:
var a =200;
var b ="1";
var c= a + b;
你可能期望c是201,但實際上它是"2001",這個錯誤在強型別語言中絕對不會出現。然而正是因為JavaScript沒有這些約束,所以可以很方便地拼接數字和字串型別。所以,約束和靈活性對語言的設計者而言,永遠是需要平衡的一組特性。
類型是一種約束,這種約束是透過類型檢查來發生作用的。在不同語言中,類型檢查在不同的階段發生作用,這樣又可以分為編譯時檢查和運行時檢查。對於JavaScript這樣的解釋型語言,也有跟編譯過程比較相似的階段,即詞法分析和語法分析,解釋型語言的類型檢查若在語法分析或之前的階段完成,也可以認為類似於編譯時檢查。所以更合理的說法是靜態型別檢查和動態型別檢查。
有趣的是,很多語言雖然編譯時檢查類型,但是它的類型信息仍可以在運行時獲得,如C#中使用元數據來保存類型信息,在運行階段,用戶可以通過反射來獲取和使用類型的資訊.
JavaScript在設計的各個方面都以靈活性優先,所以它使用動態類型檢查,並且除了在進行極少數特定操作時,JavaScript不會主動檢查類型。你可以在運行時獲得任何一個變數或表達式的類型資訊並且透過程式邏輯檢查它的正確性。
JavaScript標準中規定了9種型別:Undefined Null Boolean String Number Object Reference List Completion
其中,Reference List Completion三種類型僅供語言解析運行時使用,無法從程式中直接訪問,這裡就暫不做介紹。下面我們可以了解下這六種類型:
Undefined類型只有一個值undefined,它是變數未被賦值時的值,在JS中全域物件有一個undefined屬性表示undefined,事實上undefined並非JavaScript的關鍵字,可以給全域的undefined屬性賦值來改變它的值。
Null型別也只有一個值null,但是JavaScript為它提供了一個關鍵字null來表示這個唯一的值。 Null類型的語意是「一個空的物件參考」。
Boolean有兩種取值true和false
String類型的正式解釋是一個16位元無符號整數類型的序列,它實際上是用來表示以UTF-16編碼的文字訊息。
JavaScript的Number共有18437736874454810627 (就是264-253 +3)個值。 JavaScript的Number以雙精度浮點類型存儲,除了9007199254740990表示NaN,它遵守IEEE 754(見附1)規定,佔用64位元8位元組。
JavaScript中最複雜的型別就是Object,它是一系列屬性的無序集合,Function是實作了私有屬性[[call]]的Object,JavaScript的宿主也可以提供一些特別的物件。
前面講了JS標準中規定的類型,然而一個不能忽略的問題是JS標準是寫給JS實現者看的,對JS使用者而言,類型並不一定要按照標準來定義,比如,因為JS在進行.運算的時候,會自動把非Object型別轉換成與其對應的對象,所以"str".length其實和(new String("str")).length是等效的,從這個角度而言,認為二者屬於同一類型也未嘗不可。我們利用JS中的一些語言特性,可以進行運行時的類型判別,但是這些方法判斷的結果各不相同,孰好孰壞還需要您自己決定。
typeof是JS語言中的一個運算符,從它的字面來看,顯然它是用來獲取類型的,按JavaScript標準的規定,typeof獲取變量類型名稱的字符串表示,他可能得到的結果有6種:string、bool、number、undefined、object、function,而且JavaScript標準允許其實作者自訂一些物件的typeof值。
在JS標準中有這樣一個描述列表:
Type | Result |
Undefined | "undefined" |
Null | "object" |
Boolean | "boolean" |
Number | "number" |
String | "string" |
Object (native and doesn't implement [[call]]) | "object" |
Object (native and implements [[call]]) | "function" |
Object (host) | Implementation-dependent |
下面一個範例來自51js的Rimifon,它顯示了IE中typeof的結果產生"date"和"unknown"的情況:
var xml=document.createElement("xml");
var rs=xml.recordset;
rs.Fields.Append("date", 7, 1);
rs.Fields.Append("bin", 205, 1);
rs.Open();
rs.AddNew();
rs.Fields.Item("date").Value = 0;
rs.Fields.Item("bin").Value = 21704;
rs.Update();
var date = rs.Fields.Item("date").Value;
var bin = rs.Fields.Item("bin").Value;
rs.Close();
alert(date);
alert(bin);
alert([typeof date, typeof bin]);
try{alert(date.getDate())}catch(err){alert(err.message)}
關於這個最接近"類型"語意的判斷方式,實際上有不少的批評,其中之一是它無法分辨不同的object,new String("abc")和new Number(123)使用typeof無法區分,由於在JS程式設計中,往往會大量使用各種對象,而typeof對所有物件都只能給出一個模糊的結果"object",這使得它的實用性大大降低。
instanceof的意思翻譯成中文就是"是…的實例",從字面意思理解它是一個基於類別物件導向程式設計的術語,而JS實際上沒有在語言層級對基於類別的程式設計提供支援。 JavaScript標準雖然隻字未提,但其實一些內建物件的設計和運算子設定都暗示了一個"官方的"實現類別的方式,即從把函數當作類別使用,new運算子作用於函數時,將函數的prototype屬性設定為新建構物件的原型,並且將函數本身作為建構函數。
所以從同一個函數的new運算建構出的對象,被認為是一個類別的實例,這些對象的共同點是:1.有同一個原型2.經過同一個建構子處理。而instanceof正是配合這種實作類別的方式來檢查"實例是否屬於一個類別"的一種運算子。猜一猜也可以知道,若要檢查一個物件是否經過了一個構造函數處理千難萬難,但是檢查它的原型是什麼就容易多了,所以instanceof的實現從原型角度理解,就是檢查一個對象的[ [prototype]]屬性是否跟特定函數的prototype一致。注意這裡[[prototype]]是私有屬性,在SpiderMonkey(就是Firefox的JS引擎)中它可以用__proto__來存取。
原型只對於標準所描述的Object類型有意義,所以instanceof對於所有非Object物件都會得到false,而且instanceof只能判斷是否屬於某一類型,無法得到類型,但是instanceof的優勢也是顯而易見的,它能夠分辨自定義的"類別"建構出的物件。
instanceof其實是可以被欺騙的,它用到的物件私有屬性[[prototype]]固然不能更改,但函數的prototype是個共有屬性,下面程式碼展示瞭如何欺騙instanceof
function ClassA(){};
function ClassB(){};
var o = new ClassA();//建構一個A類的對象
ClassB.prototype = ClassA.prototype; //ClassB.prototype替換掉
alert(o instanceof ClassB)//true 欺騙成功- -!
Object.prototype.toString原本很難被呼叫到,所有的JavaScript內建類別都覆寫了toString這個方法,而對於非內建類別建構的對象,Object.prototype.toString又只能得到毫無意義的[object Object ]這種結果。所以在相當長的一段時間內,這個函數的神奇功效都沒有被發掘出來。
在標準中,Object.prototype.toString的描述只有3句
1. 取得this物件的[[class]]屬性
2. 連接三個字串"[object ",結果(1), 和"]"算出一個字串
3. 返回結果(2).
顯而易見,Object.prototype.toString其實只是取得物件的[[class]]屬性而已,不過不知道是不是有意為之,所有JS內建函數物件String Number Array RegExp…用於new建構物件時,都會設定[[class]]屬性,這樣[[class]]屬性就可以作為很好的判斷類型的依據。
因為Object.prototype.toString是取this物件屬性,所以只要用Object.prototype.toString.call或Object.prototype.toString.apply就可以指定this對象,然後取得型別了。
Object.prototype.toString儘管巧妙,但是卻無法取得自訂函數建構物件的類型,因為自訂函數不會設[[class]],而且這個私有屬性是無法在程式中存取的。 Object.prototype.toString最大的優點是可以讓1和new Number(1)成為相同類型的對象,大部分時候二者的使用方式是相同的。
然而值得注意的是new Boolean(false)在參與bool運算時與false結果剛好相反,如果這個時候把二者視為同一類型,容易導致難以檢查的錯誤。
為了比較上面三種類型判斷方法,我做了一張表格,大家可以因此對幾種方法有個整體比較。為了方便比較,我把幾種判斷方式得到的結果統一了寫法:
物件 | typeof | instanceof | Object.prototype.toString | 標準 |
"abc" | String | —— | String | String |
new String("abc") | Object | String | String | Object |
function hello(){} | Function | Function | Function | Object |
123 | Number | —— | Number | Number |
new Number(123) | Object | Number | Number | Object |
new Array(1,2,3) | Object | Array | Array | Object |
new MyType() | Object | MyType | Object | Object |
null | Object | —— | Object | Null |
undefined | Undefined | —— | Object | Undefined |
事實上,很難說上面哪一種方法是更加合理的,即使是標準中的規定,也只是體現了JS的運行時機製而不是最佳使用實踐。我個人觀點是淡化"類型"這個概念,而更多關注"我想如何使用這個對象"這種約束,使用typeof配合instanceof來檢查完全可以在需要的地方達到和強類型語言相同的效果。
sign bit(符號): 用來表示正負號
exponent(指數): 用來表示次方數
mantissa(尾數): 用來表示精確度