【類和模組】類和型別
類和型別
JavaScript定義了少量的資料型別:null、undefined、布林值、數字、字串、函式和物件。typeof運算子可以得出值的型別。然而,我們往往更希望將類作為型別來對待,這樣就可以根據物件所屬的類來區分它們。JavaScript語言核心中的內建物件(通常是指客戶端JavaScript的宿主物件)可以根據它們的class屬性來區分彼此。
instanceof 運算子
instanceof運算子,左運算元是待檢測其類的物件,右運算元是定義類的建構函式。如果o繼承自c.prototype,則表示式o instanceof c值為true。這裡的繼承可以不是直接繼承,如果o所繼承的物件繼承自另一個物件,後一個物件繼承自c.prototype,這個表示式的運算結果也是true。
建構函式是類的公共標識,但原型是唯一的標識。儘管instanceof運算子的右運算元是建構函式,但計算過程實際上是檢測了物件的繼承關係,而不是檢測建立物件的建構函式。
如果你想檢測物件的原型鏈上是否存在某個特定的原型物件,有沒有不使用建構函式作為中介的方法呢?答案是肯定的,可以使用isPrototypeOf()方法。比如,可以通過如下程式碼來檢測物件r是否是範圍類的成員:
range.methods.isPrototypeOf(r); // range.method是原型物件
instanceof運算子和isPrototypeOf()方法的缺點是,我們無法通過物件來獲得類名,只能檢測物件是否屬於指定的類名。在客戶端JavaScript中還有一個比較嚴重的不足,就是在多視窗和多框架子頁面的Web應用中相容性不佳。每個視窗和框架子頁面都具有單獨的執行上下文,每個上下文都包含獨有的全域性變數和一組建構函式。在兩個不同框架頁面中建立的兩個陣列繼承自兩個相同但相互獨立的原型物件,其中一個框架頁面中的陣列不是另一個框架頁面的Array()建構函式的例項,instanceof運算結果是false。
constructor屬性
另一種識別物件是否屬於某個類的方法是使用constructor屬性。因為建構函式是類的公共標識,所以最直接的方法就是使用constructor屬性,比如:
function typeAndValue(x) { if (x == null) return ""; // NUll和undefined沒有建構函式 switch(x.constructor) { case Number: return "Number: " + x; // 處理原始型別 case String: return "String: '" + x + "'"; case Date: return "Date: " + x; // 處理內建函式 case RegExp: return "Regexp: " + x; case Complex: return "Complex: " + x; // 處理自定義型別 } }
需要注意的是,在程式碼中關鍵字case後的表示式都是函式,如果改用typeof運算子或獲取到物件的class屬性的話,它們應當改為字串。
使用constructor屬性檢測物件屬於某個類的技術的不足之處和instanceof一樣。在多個執行上下文的場景中它是無法正常工作的(比如在瀏覽器視窗的多個框架子頁面中)。在這種情況下,每個框架頁面各自擁有獨立的建構函式集合,一個框架頁面中的Array建構函式和另一個框架頁面的Array建構函式不是同一個建構函式。
同樣,在JavaScript中也並非所有的物件都包含constructor屬性。在每個新建立的函式原型上預設會有constructor屬性,但我們常常會忽覺原型上的constructor屬性。
建構函式的名稱
使用instanceof運算子和constructor屬性來檢測物件所屬的類有一個主要的問題,在多個執行上下文中存在建構函式的多個副本的時候,這兩種方法的檢測結果會出錯。多個執行上下文中的函式看起來是一模一樣的,但它們是相互獨立的物件,因此彼此也不相等。
一種可能的解決方案是使用建構函式的名字而不是建構函式本身作為類識別符號。一個窗口裡的Array建構函式和另一個視窗的Array建構函式是不相等的,但是它們的名字是一樣的。在一些JavaScript的實現中為函式物件提供了一個非標準的屬性name,用來表示函式的名稱。對於那些沒有name屬性的JavaScript實現來說,可以將函式轉換為字串,然後從中提取出函式名。
下例定義的type()函式以字串的形式返回物件的型別。它用typeof運算子來處理原始值和函式。對於物件來說,它要麼返回class屬性的值要麼返回建構函式的名字。
例:可以判斷值的型別的type()函式 /** * 以字串形式返回o的型別: * -如果o是null,返回"null";如果o是NaN,返回"nan" * -如果typeof返回的值不是"object",則返回這個值 * (注意,有一些JavaScript的實現將正則表示式識別為函式) * - 如果o的類不是"Object",則返回這個值 * - 如果o包含建構函式並且這個建構函式具有名稱,則返回這個名稱 * - 否則,一律返回“Object” **/ function type(o) { var t, c, n; // type, class, name // 處理null值的特殊情形 if (o === null) return "null"; // 另外一種特殊情形:NaN和它自身不相等 if (o !== o) return "nan"; // 如果typeof的值不是"object",則使用這個值 // 這可以識別出原始值的型別和函式 if ((t = typeof o) !== "object") return t; // 返回物件的類名,除非值為“Object” // 這種方式可以識別出大多數的內建物件 if ((c = classof(o)) !== "Object") return c; // 如果物件建構函式的名字存在的話,則返回它 if (o.constructor && typeof o.constructor === "function" && (n = o.constructor.getName())) return n; // 其他的型別都無法判別,一律返回"Object" return "Object"; } // 返回物件的類 function classof(o) { return Object.prototype.toString.call(o).slice(8, -1); }; // 返回函式的名字(可能是空字串),不是函式的話返回null Function.prototype.getName = function () { if ("name" in this) return this.name; return this.name = this.toString().match(/function\s*([^(]*)\(/)[1]; };
這種使用建構函式名字來識別物件的類的做法和使用constructor屬性一樣有一個問題:並不是所有的物件都具有constructor屬性。此外,並不是所有的函式都有名字。如果使用不帶名字的函式定義表示式定義一個建構函式,getName()方法則會返回空字串:
// 這個建構函式沒有名字 var Complex = function(x,y) { this.r = x; this.i = y; } // 這個建構函式有名字 var Range = function Range(f,t) { this.from = f; this.to = t; }
鴨式辯型
上文所描述的檢測物件的類的各種技術多少都會有些問題,至少在客戶端JavaScript中是如此。解決辦法就是規避掉這些問題:不要關注“物件的類是什麼”,而是關注“物件能做什麼”。這種思考問題的方式在Python和Ruby中非常普遍,稱為“鴨式辯型”(這個表述是由作家James Whitcomb Riley 提出的)。
像鴨子一樣走路、游泳並且嘎嘎叫的鳥就是鴨子。
對於JavaScript程式設計師來說,這句話可以理解為“如果一個物件可以像鴨子一樣走路、游泳並且嘎嘎叫,就認為這個物件是鴨子,哪怕它並不是從鴨子類的原型物件繼承而來的”。
鴨式辯型的實現方法讓人感覺太“放任自流”:僅僅是假設輸入物件實現了必要的方法,根本沒有執行進一步的檢査。如果輸入物件沒有遵循“假設”,那麼當代碼試圖呼叫那些不存在的方法時就會報錯。另一種實現方法是對輸入物件進行檢査。但不是檢査它們的類,而是用適當的名字來檢査它們所實現的方法。這樣可以將非法輸入儘可能早地攔截在外,並可給出帶有更多提示資訊的報錯。
下例中按照鴨式辯型的理念定義了quacks()函式(函式名叫“implements”會更加合適,但implements是保留字)。quacks()用以檢査一個物件(第一個實參)是否實現了剩下的引數所表示的方法。對於除第一個引數外的每個引數,如果是字串的話則直接檢査是否存在以它命名的方法;如果是物件的話則檢査第一個物件中的方法是否在這個物件中也具有同名的方法;如果引數是函式,則假定它是建構函式,函式將檢査第一個物件實現的方法是否在建構函式的原型物件中也具有同名的方法。
例:利用鴨式辯型實現的函式 //如果o實現了除第一個引數之外的引數所表示的方法,則返回true function quacks(o /*, ... */) { for (var i = 1; i < arguments.length; i++) { // 遍歷o之後的所有引數 var arg = arguments[i]; switch (typeof arg) { // 如果引數是: case 'string': // string: 直接用名字做檢査 if (typeof o[arg] !== "function") return false; continue; case 'function': // function: 檢査函式的原型物件上的方法 // 如果實參是函式,則使用它的原型 arg = arg.prototype; // 進入下一個case case 'object': // object:檢査匹配的方法 for (var m in arg) { // 遍歷物件而每個屬性 if (typeof arg[m] !== "function") continue; // 跳過不是方法的屬性 if (typeof o[m] !== "function") return false; } } } // 如果程式能執行到這裡,說明o實現了所有的方法 return true; }
關於這個
quacks()
函式還有一些地方是需要尤為注意的。首先,這裡只是通過特定的名稱來檢測物件是否含有一個或多個值為函式的屬性。我們無法得知這些已經存在的屬性的細節資訊,比如,函式是幹什麼用的?它們需要多少引數?引數型別是什麼?然而這是鴨式辯型的本質所在,如果使用鴨式辯型而不是強制的型別檢測的方式定義API,那麼建立的API應當更具靈活性才可以,這樣才能確保你提供給使用者的API更加安全可靠。關於quacks()
函式還有另一問題需要注意,就是它不能應用於內建類。比如,不能通過quacks(o,Array)
來檢測o是否實現了Array中所有同名的方法。原因是內建類的方法都是 不可列舉的,quacks()
中的for/in迴圈無法遍歷到它們(注意,在ECMAScript 5中有一 個補救辦法,就是使用ojbeet.getOwnPropertyNames()
)。