MooTools Class 使用、繼承詳解

 ? 首先需要說明的是,本文將直接講解創建和使用類的各種技巧,一些基礎的東西不再做解釋,要理解如何在JavaScript中實現面向對象的設計,請先參考《JavaScript.高級程序設計(第2版)》(前7章)、《javascript.設計模式》(前四章)、《JavaScript.語言精粹》這三部經典之作。

 
? 在Mootools中使用Class構造函數創建一個類有兩種方式,也就是傳遞兩種不同類型的參數給構造函數,第一種也是標準方式傳遞一個對象字面量,這個對象字面量可以包括你爲類添加的所有屬性、方法。例如:
 
  1. var Person = new Class({ 
  2.     // Methods 
  3.     initialize: function (name, age) { 
  4.         this.name = name; 
  5.         this.age = age; 
  6.     }, 
  7.  
  8.     log: function () { 
  9.         console.log(this.name + ',' + this.age); 
  10.     } 
  11. }); 
  12.  
  13. var mark = new Person('mark', 24); 
  14. mark.log(); // returns 'mark,24' 
? 第二種是傳遞一個普通函數,mootools會自動把這個函數包裝成只含一個initialize鍵值的對象字面量,然後你可以使用implement方法對類進行擴展,例如:
 
  1. var Person = new Class(function (name, age) { 
  2.     this.name = name; 
  3.     this.age = age; 
  4. }); 
  5. Person.implement('log'function () { 
  6.     console.log(this.name + ',' + this.age); 
  7. }); 
  8. var mark = new Person('mark', 24); 
  9. mark.log(); // returns 'mark,24' 
? 當然推薦使用的還是第一種方式,直觀明瞭嘛。你如果使用標準方式建立了一個類,也是可以使用implement方法對類進行擴展的,如果你真的認爲有必要把一個類的設計拆成幾個部分的話(例如在使用摻元對象實現多親繼承時,神馬?等等...這個MooTools裏實現多親繼承繼承不是使用Implements Mutator嗎,嗯吶,這個在接下來類的繼承中將詳細講解......),呵呵,至於MooTools內部如果對構造函數進行解析,有興趣的可看看MooTools 1.4 源碼分析 - Class 修正版
 
 
? Implement and Extend
 
? Implement方法用來爲類添加新的方法、屬性。需要注意的是,如果新添加的方法或屬性與類中舊有的方法或屬性同名,則會覆蓋類中舊有的方法、屬性。調用Implement方法有兩種方式,第一種方式傳遞兩個參數,第一個參數爲String類型,存儲要添加的方法或屬性的名稱,第二個參數爲方法所對應的函數或屬性所對應的值,這種方式每次只能爲類添加一個方法或屬性:
 
  1. Person.implement('log'function () { 
  2.     console.log(this.name + ',' + this.age); 
  3. }); 
  4. Person.implement('city''深圳'); 
? 第二種方式傳遞一個對象字面量參數,把要添加的方法屬性包含在這個對象中,一次添加多個方法、屬性,避免重複調用implement:
 
  1. Person.implement({ 
  2.     'city''深圳'
  3.     'log'function () { 
  4.         console.log(this.name + ',' + this.age); 
  5.     } 
  6. }); 
? MooTools關於Class的官方文檔中只暴露了implement一個方法,其實對類本身進行操作的還有一個比較重要的方法extend,這個方法之所以沒有出現在Class的文檔中。這是因爲它不是作爲Class的特殊方法,而實際上是Type的方法。它的作用是爲類創建靜態成員,靜態成員關聯的是類本身,換句話說,靜態成員是在類的層次上操作,而不是在實例的層次上操作,每個靜態成員都只有一份。調用extend方法的方式同Implement,也是兩種方式。
 
? 簡單一點講,implement爲實例創建方法和屬性,extend爲類本身創建方法和變量,請看下面的例子:
 
  1. var Person = new Class(function (name, age) { 
  2.     this.name = name; 
  3.     this.age = age; 
  4. }); 
  5. Person.implement({ 
  6.     instanceMethod: function () { 
  7.         console.log('From an instance!'); 
  8.     } 
  9. }); 
  10. Person.extend({ 
  11.     classMethod: function () { 
  12.         console.log('From the class itself!'); 
  13.     } 
  14. }); 
  15.  
  16. var mark = new Person('mark', 24); 
  17.  
  18. console.log(typeOf(mark.instanceMethod)); // returns 'function' 
  19. mark.instanceMethod(); // returns 'From an instance!' 
  20. console.log(typeOf(mark.classMethod)); // returns 'null',說明實例是不能調用靜態方法的 
  21.  
  22. console.log(typeOf(Person.classMethod)); // returns 'function' 
  23. Person.classMethod(); // returns 'From the class itself!' 
  24. console.log(typeOf(Person.instanceMethod)); // returns 'null',同樣類也不能直接調用爲實例而創建的方法 
  25.  
  26. Person.prototype.instanceMethod(); // 類只能通過這種方式調用原型上的方法 
 
 
? 私有成員
 
? 嚴格來講,JavaScript中沒有私有成員的概念,所有對象的屬性都是共有的。不過,倒是有一個私有變量的概念,任何在函數中定義的變量,都可以認爲是私有變量,因爲不能在函數的外部訪問浙西變量。私有變量包括函數的參數、局部變量和在函數內定義的其他函數。所以我們可以通過使用閉包來爲類製造私有成員:
 
  1. var Person = (function () { 
  2.     // 私有變量 
  3.     var numOfPersons = 0; 
  4.  
  5.     // 私有方法 
  6.     var formatName = function (name) { 
  7.         return name.capitalize(); 
  8.     }; 
  9.  
  10.     return new Class({ 
  11.         initialize: function (name, age) { 
  12.             this.name = name; 
  13.             this.age = age; 
  14.             numOfPersons++; 
  15.         }, 
  16.  
  17.         // 公有方法 
  18.         log: function () { 
  19.             console.log(formatName(this.name) + ',' + this.age); 
  20.         }, 
  21.  
  22.         getNumOfPersons: function () { 
  23.             return numOfPersons; 
  24.         } 
  25.     }); 
  26. })(); 
  27.  
  28. var mark = new Person('mark', 24); 
  29. mark.log(); // returns 'mark,24' 
  30. console.log(mark.getNumOfPersons()); // returns 1 
? 使用這個模式有一個好處就是,私有成員在內存中只會存放一份,是由所有實例共享的,不必爲每一個實例生成一個副本。但這也延伸出一個問題,來看下面的代碼:
 
  1. var Person = (function () { 
  2.     // 私有變量 
  3.     var name = ''
  4.  
  5.     return new Class({ 
  6.         initialize: function (v1, v2) { 
  7.             name = v1; 
  8.             this.age = v2; 
  9.         }, 
  10.  
  11.         getName: function () { 
  12.             return name; 
  13.         }, 
  14.  
  15.         setName: function (value) { 
  16.             name = value; 
  17.         }, 
  18.  
  19.         getAge: function () { 
  20.             return this.age; 
  21.         }, 
  22.  
  23.         setAge: function (value) { 
  24.             this.age = value; 
  25.         } 
  26.     }); 
  27. })(); 
  28.  
  29. var mark = new Person('mark', 24); 
  30. console.log(mark.getName()); // 'mark' 
  31. mark.setName('grey'); 
  32. console.log(mark.getName()); // 'grey' 
  33. console.log(mark.getAge()); // 24 
  34.  
  35. var john = new Person('john', 18); 
  36. console.log(john.getName()); // 'john' 
  37. console.log(john.getAge()); // 18 
  38. console.log(mark.getName()); // 'john' 
  39. console.log(mark.getAge()); // 24 
? 這個例子中的Person構造函數(這裏指initialize)與getName()和setName()方法一樣,都有權訪問私有變量name,在這種模式下,變量name就變成了一個靜態的、有所有實例共享的屬性,也就是說,在一個實例上調用setName()會影響所有實例,結果就是所有實例getName()都會返回相同的值,而age是實例變量就不存在這個問題。到底是使用實例變量還是靜態私有變量,最終還是要視你的需求而定。
 
? 當然上面這個問題只是針對私有變量的,私有方法就不存在這個問題,相比實例方法會更有效率(從內存佔用的意義上來說),應爲它只會被創建一份。
 
? 使用閉包還帶來一個問題,多查找作用域鏈中的一個層次,就會在一定程度上影響查找的速度(一般情況下可以忽略不計),魚與熊掌不可兼得啊......
 
 
? 常量
 
? 最簡單設置常量的方法是爲類添加一個靜態屬性,然而靜態屬性是公有的,類的使用者可以隨時改變它的值,這個樣的操作後果是很嚴重的。這裏我們可以使用前面介紹的爲類設置靜態私有變量的方式來模擬常量,然後在實例方法中只創建取值器方法而不創建賦值器方法。這樣類的使用者只能使用暴露出來的取值器方法來得到私有變量的值而不能改變它的值。來看下面的代碼:
 
  1. var Person = (function () { 
  2.     // 私有變量 
  3.     var AGE_UPPER_BOUND = 32; 
  4.  
  5.     return new Class({ 
  6.         initialize: function (v1, v2) { 
  7.             // ... 
  8.         }, 
  9.  
  10.         getAGEUPPERBOUND: function (value) { 
  11.             return AGE_UPPER_BOUND; 
  12.         } 
  13.     }); 
  14. })(); 
? 如果需要使用多個常量,設置一個私有的對象字面量來存儲這些常量,然後設置一個通用的取值器方法來取得這些常量:
 
  1. var Person = (function () { 
  2.     // 私有變量 
  3.     var constants = { 
  4.         AGE_UPPER_BOUND: 32, 
  5.         AGE_LOWER_BOUND: 18 
  6.     }; 
  7.  
  8.     return new Class({ 
  9.         initialize: function (v1, v2) { 
  10.             // ... 
  11.         }, 
  12.  
  13.         getConstants: function (name) { 
  14.             return constants[name]; 
  15.         } 
  16.     }); 
  17. })(); 
 
 
? 繼承
 
? 繼承的主要好處表現在代碼的重用方面,通過建立類之間的繼承關係,有些方法我們只需要定義一次就可以了。同樣,如果需要修改這些方法或排查其中的錯誤,那麼由於其定義只出現在一個位置,所以非常有利於節省時間和精力。MooTools實現類的繼承有兩種模式:原型式繼承和多親繼承,原型式繼承由內建的Extends Mutator來實現,多親繼承由Implements Mutator或implement方法來實現。
 
? 原型式繼承這裏就不多講了,着重講一下多親繼承。在JavaScript裏,因爲一個對象只能擁有一個原型對象,所以不允許子類繼承多個超類,不過我們可以利用多個摻元類(minix class)或摻元對象對一個類進行擴充,這樣類的實例就可以擁有mixin類(對象)中的方法、屬性,所以這實際上實現了多繼承的效果。通常mixin類(對象)包含一些通用的方法,大家可以看看MooTools裏Class.Extras模塊中三個mixin類的定義(Chain、Events、Options)。
 
? 這裏還需要注意的的一點是,在派生具有私有成員的的類或implement mixin類(對象)時,因爲在父類(或mixin)中訪問這些私有變量的方法是公有的,所以他們也會被遺傳下來。所以子類可以間接訪問父類(或mixin)的私有成員,但子類自身的實例方法都不能直接訪問這些私有屬性,而且你也不能在子類中添加能夠直接訪問他們的方法(作用域變量,你想訪問也訪問不了啦,呵呵)。
 
? 首先我們先建立一個基類:
 
  1. var Animal = new Class({ 
  2.     initialize: function (age) { 
  3.         this.age = age; 
  4.     } 
  5. }); 
? 使用Extends Mutator派生一個子類:
 
  1. var Cat = new Class({ 
  2.     Extends: Animal, 
  3.     initialize: function (name, age) { 
  4.         this.parent(age); // calls initalize method of Animal class 
  5.         this.name = name; 
  6.     } 
  7. }); 
  8.  
  9. var cat = new Cat('Micia', 20); 
  10. console.log(cat.name); // 'Micia' 
  11. console.log(cat.age); // 20 
? 利用Implements Mutator擴充一個類,首先建立一個mixin類:
 
  1. var Mixin = new Class({ 
  2.     getName: function () { 
  3.         return this.name; 
  4.     }, 
  5.     setName: function (value) { 
  6.         this.name = value 
  7.     } 
  8. }); 
  9.  
  10. var Cat = new Class({ 
  11.     Extends: Animal, 
  12.     Implements: Mixin, 
  13.     initialize: function (name, age) { 
  14.         this.parent(age); // calls initalize method of Animal class 
  15.         this.name = name; 
  16.     } 
  17. }); 
  18.  
  19. var cat = new Cat('Micia', 20); 
  20. console.log(cat.name); // 'Micia' 
  21. console.log(cat.age); // 20 
  22. cat.setName('Dog'); 
  23. console.log(cat.getName()); // 'Dog' 
? 使用implement方法擴充一個類,首先家裏一個mixin對象:
 
  1. // mixin對象存儲一些通用方法,可以被不同的類implement 
  2. var objMixin = (function () { 
  3.     var counter = 0; 
  4.  
  5.     return { 
  6.         init: function () { 
  7.             counter += 1; 
  8.         }, 
  9.         getCounter: function () { 
  10.             return counter; 
  11.         }, 
  12.         getAge: function () { 
  13.             return this.age; 
  14.         }, 
  15.         setAge: function (value) { 
  16.             this.age = value; 
  17.         } 
  18.     }; 
  19. })(); 
  20.  
  21. var Cat = new Class({ 
  22.     Extends: Animal, 
  23.     Implements: Mixin, 
  24.     initialize: function (name, age) { 
  25.         this.parent(age); // calls initalize method of Animal class 
  26.         this.name = name; 
  27.     } 
  28. }); 
  29. Cat.implement(objMixin); 
  30.  
  31. var Dog = new Class({ 
  32.     Extends: Animal, 
  33.     Implements: Mixin, 
  34.     initialize: function (name, age) { 
  35.         this.parent(age); // calls initalize method of Animal class 
  36.         this.name = name; 
  37.     } 
  38. }); 
  39. Dog.implement(objMixin); 
  40.  
  41. var cat = new Cat('Micia', 20); 
  42. console.log(cat.name); // 'Micia' 
  43. console.log(cat.age); // 20 
  44. cat.setName('湯姆'); 
  45. console.log(cat.getName()); // '湯姆' 
  46. cat.setAge(12); 
  47. console.log(cat.getAge()); // 12 
  48. // 對mixin對象的私有屬性進行操作 
  49. cat.init(); 
  50. console.log(cat.getCounter()); // 1 
  51.  
  52. var dog = new Dog('小狗', 6); 
  53. console.log(dog.name); // '小狗' 
  54. console.log(dog.age); // 6 
  55. dog.setName('布魯托'); 
  56. console.log(dog.getName()); // '布魯托' 
  57. dog.setAge(8); 
  58. console.log(cat.getAge()); // 8 
  59. // 對mixin對象的私有屬性進行操作 
  60. dog.init(); 
  61. console.log(dog.getCounter()); // 2 
  62. console.log(cat.getCounter()); // 2 
? 大家都看明白了吧,呵呵,不過通過上面的代碼我們引申出另外一個問題,注意上面的Cat類的設計,我們首先設計了Extends,然後是Implements,再就是Cat類本身的方法屬性,MooTools內部對Class構造函數解析時是按照我們設計時的順序解析的嗎?答案是按照我們設計時的順序解釋的。簡單來講MooTools通過for-in對對象進行枚舉來遍歷每個成員進行解釋的,等等......那個ECMAScript最新版對for-in 語句的遍歷機制又做了調整,屬性遍歷的順序是沒有被規定的,也就是說隨機的,那麼MooTools是怎樣保證按順序解釋的呢?先看下面這段代碼:
 
  1. var obj = { 
  2.     Waa: "Waa"
  3.     aa: 'aa'
  4.     68: '68'
  5.     15: '15'
  6.     tt: 'tt'
  7.     '-7''-7'
  8.     _: "___"
  9.     online: true 
  10. }; 
  11. for (var k in obj) { 
  12.     console.log(k); 
? 把它放在各個瀏覽器都執行一遍,你會發現IE、火狐、Safari瀏覽器的JavaScript 解析引擎遵循的是較老的ECMA-262第三版規範,屬性遍歷順序由屬性構建的順序決定,而Chrome、Opera中使用 for-in 語句遍歷對象屬性時會遵循一個規律,它們會先提取所有 key 的 parseFloat 值爲非負整數的屬性, 然後根據數字順序對屬性排序首先遍歷出來,然後按照對象定義的順序遍歷餘下的所有屬性。其它瀏覽器則完全按照對象定義的順序遍歷屬性。
 
? 這下明白了吧,只要你爲類設計的方法、屬性還有Mutator的名稱不爲數字就可以了(當然如果你非要有這樣的嗜好,我也只能@#%&$......)。請看下面的代碼:
 
  1. var Super = new Class({ 
  2.     log: function () { 
  3.         console.log('Super'); 
  4.     } 
  5. }); 
  6.  
  7. var Mixin = new Class({ 
  8.     log: function () { 
  9.         console.log('Mixin'); 
  10.     } 
  11. }); 
  12.  
  13. var Sub = new Class({ 
  14.     Extends: Super, 
  15.     Implements: Mixin 
  16. }); 
  17.  
  18. var obj = new Sub(); 
  19. obj.log(); // ? 
? 在這裏obj.log()會返回什麼呢?對了是'Maxin',這裏Sub類首先繼承了Super類,Sub的原型實際就是Super類的一個實例,Super的log方法也就是成了Sub的原型上的一個方法,然後執行Implements Mutator 爲Sub類的原型擴展了一個Mixin類的實例上的方法,這時Mixin類實例上的log方法就覆蓋了Sub類原型上原來的log方法(繼承自Super類)。
 
? 如果把Extends、Implements的順序顛倒一下:
 
  1. var Sub = new Class({ 
  2.     Implements: Mixin, 
  3.     Extends: Super 
  4. }); 
  5.  
  6. var obj = new Sub(); 
  7. obj.log(); // ? 
? 這時obj.log()會返回什麼呢?還是'Maxin'嗎?其實這裏返回的是'Super',Why?前面我們介紹了MooTools對Class構造函數解析時是按照我們設計的順序解析的,所以在這裏首先執行的是Implements Mutator,它首先爲Sub類的原型擴展了一個Mixin類的實例上的log方法,然後纔是對超類Super的繼承,因爲在JavaScrpt裏每個對象只有一個原型,原型式繼承的原理就是超類的一個實例賦予子類的原型,子類原來的原型這時會被超類的實例替換掉,所以這是Sub類原型的引用已經指向了超類的實例,而他自己的原型對象這時被消除了,所以之前從Mixin類得來的那個log方法,對不起跟着一起魂飛湮滅了,所以這裏返回的是'Super'。
 
? 當然如果你嫌不過癮,那就在爲Sub類添加一個log方法:
 
  1. var Sub = new Class({ 
  2.     Implements: Mixin, 
  3.     Extends: Super, 
  4.  
  5.     log: function () { 
  6.         console.log('sub'); 
  7.     } 
  8. }); 
  9. var obj = new Sub(); 
  10. obj.log(); // ? 
? 你可以把Sub類的Implements、Extends、log來回顛倒一下看看效果,呵呵,再用implement方法在擴展一個試試:
 
  1. var objMixin = { 
  2.     log: function () { 
  3.         console.log('objMixin'); 
  4.     } 
  5. }; 
  6.  
  7. var Sub = new Class({ 
  8.     Implements: Mixin, 
  9.     Extends: Super, 
  10.  
  11.     log: function () { 
  12.         console.log('sub'); 
  13.     } 
  14. }); 
  15. Sub.implement(objMixin); 
  16.  
  17. var obj = new Sub(); 
  18. obj.log(); // ? 
? 呵呵,別暈掉,一切都是爲了把問題搞的跟明白不是......
 
? 最後不要忘記兩個重要的方法:parent()和protect(),這裏就不多說了,在前面的Class源碼分析裏有詳細介紹。
 
? 下一篇在詳細講解一下Mutators。
 
? 苦苦的苦瓜 2011-10-07
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章