JavaScript中的類型
一、關于類型
什么叫做類型?簡單地說,類型就是把內存中的一個二進制序列賦予某種意義。比如,二進制序列0100 0000 0111 0000 0001 0101 0100 1011 1100 0110 1010 0111 1110 1111 1001 1110如果看作是64位無符號整數類型就是4643234631018606494 而按照IEEE 754規定的浮點數二進制表示規則(見附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標準規定的類型
JavaScript標準中規定了9種類型:Undefined Null Boolean String Number Object Reference List Completion
其中,Reference List Completion三種類型僅供語言解析運行時使用,無法從程序中直接訪問,這里就暫不做介紹。下面我們可以了解下這六種類型:
Undefined類型
Undefined類型只有一個值undefined,它是變量未被賦值時的值,在JS中全局對象有一個undefined屬性表示undefined,事實上undefined并非JavaScript的關鍵字,可以給全局的undefined屬性賦值來改變它的值。
Null類型
Null類型也只有一個值null,但是JavaScript為它提供了一個關鍵字null來表示這個唯一的值。Null類型的語義是“一個空的對象引用”。
Boolean類型
Boolean有兩種取值true和false
String類型
String類型的的正式解釋是一個16位無符號整數類型的序列,它實際上用來表示以UTF-16編碼的文本信息。
Number類型
JavaScript的Number共有18437736874454810627 (就是 264-253 +3)個值。JavaScript的Number以雙精度浮點類型存儲,除了9007199254740990表示NaN,它遵守IEEE 754(見附1)規定,占用64位8字節。
Object類型
JavaScript中最為復雜的類型就是Object,它是一系列屬性的無序集合,Function是實現了私有屬性[[call]]的Object,JavaScript的宿主也可以提供一些特別的對象。
三、JavaScript使用者眼中的類型:
前面講了JS標準中規定的類型,然而一個不能忽略的問題是JS標準是寫給JS實現者看的,對JS使用者而言,類型并不一定要按照標準來定義,比如,因為JS在進行.運算的時候,會自動把非Object類型轉換為與其對應的對象,所以"str".length其實和(new String("str")).length是等效的,從這個角度而言,認為二者屬于同一類型也未嘗不可。我們利用JS中的一些語言特性,可以進行運行時的類型判別,但是這些方法判斷的結果各不相同,孰好孰壞還需要您自己決定。
typeof——看上去很官方
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——原型還是類型?
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——是個好方法?
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來檢查完全可以在需要的地方達到和強類型語言相同的效果。
附1 IEEE 754 規定的雙精度浮點數表示(來自中文wikipedia):
sign bit(符號): 用來表示正負號
exponent(指數): 用來表示次方數
mantissa(尾數): 用來表示精確度