文章目錄
- 一、關於類型
- 二、JavaScript標準規定的類型
- 三、JavaScript使用者眼中的類型:
- 附1 IEEE 754 規定的雙精確度浮點數表示(來自中文wikipedia):
一、關於類型
什麼叫做類型?簡單地說,類型就是把記憶體中的一個二進位序列賦予某種意義。比如,二進位序列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(尾數): 用來表示精確度