MooTools Class.Mutators 如何建立一個我們自己的Mutator

Mutator是一個可以改變你的類的結構的一個很特殊的函數,它們是產生特別功能和優雅化繼承和摻元的的有力工具。MooTools有兩個內建的Mutators: Extends和Implements:Extends Mutator取得傳給它的類的名字,然後直接繼承它;而Implements取得傳給它的摻元類(Mixin Class)的名字後,把那些類的方法添加到新類中。這兩個Mutator如何使用可參看前面的博文MooTools Class 使用、繼承詳解

Mootools把Mutators儲存在Class.Mutators對象中,新建一個Mutator就是爲Class.Mutators對象添加一個鍵,鍵名爲Mutator的關鍵字(既Mutator的名字),鍵值爲Mutator的實際函數。

在MooTools中Mutators是基於key-to-key對應的方式進行工作的,MooTools在構建一個新類時,會檢查傳送給新類的構造函數的對象的每一個鍵,在Class.Mutators對象中是不是有mutator函數的對應的名字在裏面。如果找到了,它就調用這個函數並且把鍵的值傳給它做處理。所以爲了使你新建的Class中使用一個Mutator,你必須在傳送給類的構造函數的對象上有一個與這個mutator相同名字的一對鍵值(例如,要使用Extends Mutator來實現類的繼承,必須在你的類的聲明對象上寫Extends:ParentClassName,這個比較好理解)。

這裏有兩點需要注意:

首先,MooTools提供了兩個Mutators: Extends和Implements,如果你使用MooTools-More的話,還有一個Binds Mutator。你新建的Mutator的關鍵字不能與上面三個Mutator重名,那樣的後果簡直無法想象,呵呵......

其次,你新建一個Mutator時不能使用對象字面量的方式建立,看看下面代碼:
 

  1. Class.Mutators = {  
  2.     Keyword1: function (argus) {  
  3.         ...  
  4.     },  
  5.  
  6.     Keyword2: function (argus) {  
  7.         ...  
  8.     }  
  9. };  

 爲什麼?因爲這樣語法,相當於以對象字面量形式創建了一個新的對象然後在賦予Class.Mutators,本質上完全重寫了Class.Mutators,這樣MooTools提供的Mutators(包括你之前建立的)都會消失不見,還想要繼承?還想要摻元?呵呵......正確的代碼是這樣:
 

  1. Class.Mutators.Keyword1 = function (argus) {  
  2.     ...  
  3. };  
  4.  
  5. Class.Mutators.Keyword2 = function (argus) {  
  6.     ...  
  7. };  

好了說了那麼多,只是爲了明白Mutators運行原理,接下來介紹幾個比較實用的Mutators:


Statics Mutator

MooTools Class 使用、繼承詳解中我們講解了怎樣使用extend方法爲類添加靜態成員。首先你要先建立一個類,然後才能調用類的extend方法添加靜態成員,這樣兩段代碼是分離的,來看下面代碼:
 

  1. var Person = new Class({  
  2.     initialize: function (name, age) {  
  3.         this.name = name;  
  4.         this.age = age;  
  5.     },  
  6.  
  7.     log: function () {  
  8.         console.log(this.name + ',' + this.age);  
  9.     }  
  10. });  
  11.  
  12. // 添加靜態成員  
  13. Person.extend({  
  14.     count: 0,  
  15.  
  16.     addPerson: function () {  
  17.         this.count += 1;  
  18.     },  
  19.  
  20.     getCount: function () {  
  21.         console.log('Person count: ' + this.count);  
  22.     }  
  23. });  
  24.  
  25. // 建立一個Person類的實例  
  26. var mark = new Person('Mark', 23);  
  27. mark.log();  
  28.  
  29. // 訪問Person類的靜態方法  
  30. Person.addPerson();  
  31. Person.getCount(); // returns: 1  

我如果想在傳送給類的構造函數的對象中爲類添加靜態成員呢?我們建立一個簡單的Mutator就可以了,來看下面代碼:
 

  1. Class.Mutators.Static = function (items) {  
  2.     this.extend(items);  
  3. };  
  4.  
  5. var Person = new Class({  
  6.     // 添加靜態成員  
  7.     Static: {  
  8.         count: 0,  
  9.  
  10.         addPerson: function () {  
  11.             this.count += 1;  
  12.         },  
  13.  
  14.         getCount: function () {  
  15.             console.log('Person count: ' + this.count);  
  16.         }  
  17.     },  
  18.  
  19.     initialize: function (name, age) {  
  20.         this.name = name;  
  21.         this.age = age;  
  22.     },  
  23.  
  24.     log: function () {  
  25.         console.log(this.name + ',' + this.age);  
  26.     }  
  27. });  
  28.  
  29. // 建立一個Person類的實例  
  30. var mark = new Person('Mark', 18);  
  31. mark.log();  
  32.  
  33. // 訪問Person類的靜態方法  
  34. Person.addPerson();  
  35. Person.getCount(); // returns: 1  

當MooTools在解析Person的構造函數時,會發現傳遞給它的對象中的鍵名字:Satatic。因爲我們有一個有相同名字的新的Mutator,Class就調用這個Mutator函數並且把鍵值傳給它(在這個例子中是一個有屬性和方法的對象)。我們的Static Mutator非常簡單,使用this.extends把傳過來的的對象的屬性和方法變成Class的屬性和方法(Mutator函數總是綁定到class本身上的,因此this指向你的類)。


GetterSetter Mutator

之前我們講過,在JavaScript中沒有私有成員的概念,所有對象的屬性都是共有的。那麼我們需要爲Class添加私有變量進行屬性封裝怎麼實現呢,前面在MooTools Class 使用、繼承詳解中我們介紹了使用靜態私有變量的方法,但這種方法所建立的私有變量是爲類的所有實例共享的。所以一般情況下我們只能通過命名規範來區別私有屬性,MooTools的風格是在類的屬性名前加一'$'標識符來表示這是一個私有變量。

通常在MooTools下實現屬性封裝是這樣實現的,看代碼示例:
 

  1. var Person = new Class({  
  2.     $name: '',  
  3.     $age: 0,  
  4.     $occupation: '',  
  5.  
  6.     setName: function (name) {  
  7.         this.$name = name;  
  8.         return this;  
  9.     },  
  10.  
  11.     getName: function () {  
  12.         return this.$name;  
  13.     },  
  14.  
  15.     setAge: function (age) {  
  16.         this.$age = age;  
  17.         return this;  
  18.     },  
  19.  
  20.     getAge: function () {  
  21.         return this.$age;  
  22.     },  
  23.  
  24.     setOccupation: function (occupation) {  
  25.         this.$occupation = occupation;  
  26.         return this;  
  27.     },  
  28.  
  29.     getOccupation: function () {  
  30.         return this.$occupation;  
  31.     }  
  32. });  
  33.  
  34. var mark = new Person();  
  35. mark.setName('Mark');  
  36. mark.setAge(23);  
  37. mark.setOccupation('JavaScript Developer');  
  38.  
  39. console.log(mark.getName() + ', ' + mark.getAge() + ': ' + mark.getOccupation());  
  40. // 'Mark, 23: JavaScript Developer'  

通過這種方式來實現封裝,如果屬性少的話還好,屬性一多你的代碼長度就客觀了,呵呵,在《Pro JavaScript with MooTools》一書中介紹了一個巧妙的Mutator,減少我們書寫的代碼量:
 

  1. Class.Mutators.GetterSetter = function (properties) {  
  2.     var klass = this// 緩存this對象,這裏指向的是Class本身,如果不緩存會怎樣?$%#&^@#......  
  3.     Array.from(properties).each(function (property) {  
  4.         var captProp = property.capitalize(),   // 把要添加的屬性名第一個字母變爲大寫  
  5.         $prop = '$' + property;          // 爲屬性名添加'$'標識符,表明這個屬性爲私有變量  
  6.  
  7.         // setter method  
  8.         klass.implement('set' + captProp, function (value) {  
  9.             this[$prop] = value;  
  10.             return this;  
  11.         });  
  12.  
  13.         // getter method  
  14.         klass.implement('get' + captProp, function (value) {  
  15.             return this[$prop];  
  16.         });  
  17.     });  
  18. };  
  19.  
  20. var Person = new Class({  
  21.     GetterSetter: ['name''age''occupation']  
  22. });  
  23.  
  24. var mark = new Person();  
  25. mark.setName('Mark');  
  26. mark.setAge(23);  
  27. mark.setOccupation('JavaScript Developer');  
  28.  
  29. console.log(mark.getName() + ', ' + mark.getAge() + ': ' + mark.getOccupation());  
  30. // 'Mark, 23: JavaScript Developer'  

看看設計一個類實現同樣的功能,使用GetterSetter Mutator是不是減少很多代碼呢。等等,這時有看官可能會說了,我們爲什麼要對屬性進行封裝呢,如果只是實現上面的功能直接爲Class添加name、age、occupation三個屬性,然後直接對它們進行訪問不就得了。嗯吶,這個嘛,上面的代碼的確體現不出封裝的好處。如果我們需要對類的某個私有變量進行一些邏輯控制,比如在setter方法中對值進行校驗、屬性值改變後我們需要觸發一個事件來應對值的改變等功能,這樣上面的GetterSetter Mutator就不能勝任了,我們就必須對他進行擴展,還好在MooTools Forge中我找到了這樣一個插件Class.Attributes,已經提供了這些功能,這樣我們只需要'拿來主義'就可以了,呵呵。這個源代碼進行了稍稍的修改:
 

  1. Class.Mutators.Attributes = function (attributes) {  
  2.  
  3.     var $setter = attributes.$setter,  
  4.         $getter = attributes.$getter;  
  5.     delete attributes.$setter;  
  6.     delete attributes.$getter;  
  7.  
  8.     this.implement({  
  9.  
  10.         /**  
  11.         * @property $attributes  
  12.         * @description storage for instance attributes  
  13.         */ 
  14.         $attributes: attributes,  
  15.  
  16.         /**  
  17.         * @method get  
  18.         * @param name {String} - attribute name  
  19.         * @description attribute getter  
  20.         */ 
  21.         get: function (name) {  
  22.             var attr = this.$attributes[name];  
  23.             if (attr) {  
  24.                 // valueFn()對屬性的值進行初始化,不需要任何參數  
  25.                 if (attr.valueFn && !attr.initialized) {  
  26.                     attr.initialized = true;  
  27.                     attr.value = attr.valueFn.call(this);  
  28.                 }  
  29.  
  30.                 // 優先執行屬性中定義的getter方法來獲取屬性的值  
  31.                 if (attr.getter) {  
  32.                     return attr.getter.call(this, attr.value);  
  33.                 } else {  
  34.                     return attr.value;  
  35.                 }  
  36.             } else {  
  37.                 // 如果get的屬性沒有定義,則通過$getter方法來取得屬性值  
  38.                 // $getter方法通過使用name的值if或switch擴展更多的屬性,不過這些屬性的readOnly,validator等方法需要自己在$getter中定義  
  39.                 return $getter ? $getter.call(this, name) : undefined;  
  40.             }  
  41.         },  
  42.  
  43.         /**  
  44.         * @method set  
  45.         * @param name {String} - attribute name  
  46.         * @param value {Object} - attribute value  
  47.         * @description attribute setter  
  48.         */ 
  49.         set: function (name, value) {  
  50.             var attr = this.$attributes[name];  
  51.             if (attr) {  
  52.                 // 首先判斷是不是隻讀屬性  
  53.                 if (!attr.readOnly) {  
  54.                     // 先緩存舊的屬性值  
  55.                     var oldVal = attr.value, newVal;  
  56.  
  57.                     // 判斷屬性的校驗函數存不存在,如果存在則通過校驗函數確認可不可以賦值  
  58.                     if (!attr.validator || attr.validator.call(this, value)) {  
  59.                         // 優先使用屬性中定義的setter方法爲屬性賦值  
  60.                         if (attr.setter) {  
  61.                             newVal = attr.setter.call(this, value);  
  62.                         } else {  
  63.                             newVal = value;  
  64.                         }  
  65.                         attr.value = newVal;  
  66.  
  67.                         // #region - Extended by 苦苦的苦瓜 -  
  68.  
  69.                         /**  
  70.                         # 如果新舊屬性值一樣,則不觸發事件  
  71.                         **/ 
  72.                         if (oldVal !== value) {  
  73.                             // 觸發自定義事件  
  74.                             this.fireEvent(name + 'Change', { newVal: newVal, oldVal: oldVal });  
  75.                         }  
  76.  
  77.                         // #endregion  
  78.                     }  
  79.                 }  
  80.             } else if ($setter) {  
  81.                 // 如果屬性沒有定義  
  82.                 if ($setter) { $setter.call(this, name, value); }  
  83.             }  
  84.         },  
  85.  
  86.         /**  
  87.         * @method setAttributes  
  88.         * @param attributes {Object} - a list of attributes to be set to the instance  
  89.         * @description set passed attributes passing it through .set method  
  90.         */ 
  91.         setAttributes: function (attributes) {  
  92.             Object.each(attributes, function (value, name) {  
  93.                 this.set(name, value);  
  94.             }, this);  
  95.         },  
  96.  
  97.         /**  
  98.         * @method getAttributes  
  99.         * @description returns a key-value object of all instance attributes  
  100.         * @returns {Object}  
  101.         */ 
  102.         getAttributes: function () {  
  103.             var attributes = Object.clone(this.$attributes);  
  104.             return attributes;  
  105.         },  
  106.  
  107.         /**  
  108.         * @method addAttributes  
  109.         * @param attributes {Object} - a list of new attributes to be added to the instance  
  110.         * @description adds list of attributes to the instance  
  111.         */ 
  112.         addAttributes: function (attributes) {  
  113.             Object.each(attributes, function (value, name) {  
  114.                 this.addAttribute(name, value);  
  115.             }, this);  
  116.         },  
  117.  
  118.         /**  
  119.         * @method addAttribute  
  120.         * @param name {String} - new attribute name  
  121.         * @param value {Object} - new attribute value  
  122.         * @description adds new attribute to the instance  
  123.         */ 
  124.         // 這裏的value屬性是一個對象,可以包含下面這些屬性方法value, valueFn(),getter(), setter(), readOnly, validator()  
  125.         addAttribute: function (name, value) {  
  126.             var attr = this.$attributes[name];  
  127.  
  128.             // #region - Extended by 苦苦的苦瓜 -  
  129.  
  130.             /**  
  131.             # 如果屬性已經存在則不覆蓋前屬性  
  132.             **/ 
  133.             if (!attr) {  
  134.                 attr = value;  
  135.             }  
  136.  
  137.             // #endregion  
  138.  
  139.             return this;  
  140.         }  
  141.  
  142.     });  
  143.  
  144. };  

功能多多,好處多多,呵呵,下面是示例代碼:
 

  1. var Product = new Class({  
  2.  
  3.     Implements: [Options, Events],  
  4.  
  5.     Attributes: {  
  6.  
  7.         hmkhan: {  
  8.             valueFn: function () {  
  9.                 return 'my name is HmKhan';  
  10.             }  
  11.         },  
  12.  
  13.         brand: {  
  14.             validator: function (val) {  
  15.                 return val.trim().length > 1;  
  16.             }  
  17.         },  
  18.  
  19.         model: {  
  20.             validator: function (val) {  
  21.                 return val.trim().length > 1;  
  22.             }  
  23.         },  
  24.  
  25.         name: {  
  26.             readOnly: true,  
  27.             getter: function () {  
  28.                 return this.get('brand') + ' ' + this.get('model');  
  29.             }  
  30.         },  
  31.  
  32.         price: {  
  33.             getter: function (val) {  
  34.                 return val * (100 - this.get('discount')) / 100  
  35.             }  
  36.         },  
  37.  
  38.         discount: {  
  39.             value: 0 // Default value  
  40.         }  
  41.  
  42.     },  
  43.  
  44.     initialize: function (attributes) {  
  45.         this.setAttributes(attributes);  
  46.     }  
  47.  
  48. });  
  49.  
  50. var product = new Product({  
  51.     brand: 'Porsche',  
  52.     model: '911',  
  53.     price: 100000,  
  54.     discount: 5  
  55. });  
  56.  
  57. console.log(product.get('name'));  
  58. console.log(product.get('price'));  
  59. product.addEvent('discountChange'function (event) {  
  60.     console.log("New discount: {newVal}% instead of {oldVal}%!".substitute(event));  
  61. });  
  62.  
  63. product.set('discount', 30); // -> alerts "New discount: 30% instead of 5!"  
  64. console.log(product.get('discount'));  
  65.  
  66. product.addAttribute('model', { value: '110' });  
  67. console.log(product.get('model'));  
  68.  
  69. console.log(product.get('hmkhan'));  

 

Binds Mutator

MooTools More提供了一個Mutator:Binds,用來綁定類的方法的作用域(至於爲什麼要綁定方法的作用域還有mootools中bind方法介紹這裏略過不說了,你懂的......)。來看下它的源代碼,很簡單:
 

  1. Class.Mutators.Binds = function (binds) {  
  2.     // 如果傳送給新類的構造函數的對象中沒有定義initialize方法,則爲新類定義一個空的initialize方法。  
  3.     if (!this.prototype.initialize) {  
  4.         this.implement('initialize'function () { });  
  5.     }  
  6.     // 把定義的Binds鍵值與類中已定義的Binds(繼承自父類)屬性值合併爲一個數組  
  7.     return Array.from(binds).concat(this.prototype.Binds || []);  
  8. };  
  9.  
  10. // 絕妙的構思,把initialize定義爲一個Mutator,這裏傳遞過來的參數就是類構造函數中的initialize(初始化)方法  
  11. Class.Mutators.initialize = function (initialize) {  
  12.     // 返回閉包作爲類的initialize方法  
  13.     return function () {  
  14.         // 綁定Binds屬性中包含的每個方法的作用域,替代原方法  
  15.         Array.from(this.Binds).each(function (name) {  
  16.             var original = this[name];  
  17.             if (original) { this[name] = original.bind(this); }  
  18.         }, this);  
  19.         // 執行類設計時構造函數中定義的initialize方法  
  20.         return initialize.apply(this, arguments);  
  21.     };  
  22. };  

爲什麼要把它拿出來單獨說一下呢,大家看看代碼,其實它是由兩個Mutators組成的:Binds和initialize。Binds Mutator把需要綁定作用域的方法名稱保存到類的Binds屬性中;initialize Mutator就耐人尋味了,我們定義一個新類時一般需要爲它定義一個initialize(初始化)方法,那麼他們兩個是什麼關係呢?還記得前面我們所說的“會檢查傳送給新類的構造函數的對象的每一個鍵,在Class.Mutators對象中是不是有mutator函數的對應的名字在裏面。如果找到了,它就調用這個函數並且把鍵的值傳給它做處理”,所以這時類構造函數中的initialize方法就作爲參數傳遞給initialize Mutator,initialize Mutator所做的工作就是生成並返回一個閉包,這個閉包執行的操作先是綁定Binds屬性中存儲的方法的作用域,然後再執行最初定義的initialize方法,MooTools會把這個返回的閉包在賦給initialize成員,實際上就是重寫構造函數的對象中定義的initialize方法。先看看下面的示例代碼:
 

  1. var MyClass = new Class({  
  2.     Binds: ['say'],  
  3.     initialize: function (element, message) {  
  4.         this.el = $(element);  
  5.         this.message = message;  
  6.     },  
  7.     monitor: function () {  
  8.         this.el.addEvent('click'this.say); //say is already bound to 'this'  
  9.     },  
  10.     stopMonitoring: function () {  
  11.         this.el.removeEvent('click'this.say);  
  12.     },  
  13.     say: function () {  
  14.         alert(this.message);  
  15.     }  
  16. });  
  17.  
  18. var my = new MyClass('btnSay''this is a test.');  
  19. my.monitor();  

Binds Mutator是在MyClass定義時就執行的,作用於定義的MyClass本身,爲MyClass的原型添加了一個Binds屬性,值爲包含'say'的一個數組,initialize Mutator也是在MyClass定義時就執行的,但它這時只是重寫了initialize方法,並沒有開始執行綁定方法作用域的操作,這些操作需要在initialize方法運行時纔會執行的,也就是建立MyClass類的實例my時才執行的,作用的是my對象,也就是說在執行綁定時,say方法綁定的是my對象。

看到這兒,大家應該對Mutators的運行和設計有了一個比較全面的瞭解了,MooTools Class設計必備利器啊......

下一篇我們介紹一下在MooTools中接口的實現。

苦苦的苦瓜  2011-10-18

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章