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時不能使用對象字面量的方式建立,看看下面代碼:
- Class.Mutators = {
- Keyword1: function (argus) {
- ...
- },
- Keyword2: function (argus) {
- ...
- }
- };
爲什麼?因爲這樣語法,相當於以對象字面量形式創建了一個新的對象然後在賦予Class.Mutators,本質上完全重寫了Class.Mutators,這樣MooTools提供的Mutators(包括你之前建立的)都會消失不見,還想要繼承?還想要摻元?呵呵......正確的代碼是這樣:
- Class.Mutators.Keyword1 = function (argus) {
- ...
- };
- Class.Mutators.Keyword2 = function (argus) {
- ...
- };
好了說了那麼多,只是爲了明白Mutators運行原理,接下來介紹幾個比較實用的Mutators:
Statics Mutator
在MooTools Class 使用、繼承詳解中我們講解了怎樣使用extend方法爲類添加靜態成員。首先你要先建立一個類,然後才能調用類的extend方法添加靜態成員,這樣兩段代碼是分離的,來看下面代碼:
- var Person = new Class({
- initialize: function (name, age) {
- this.name = name;
- this.age = age;
- },
- log: function () {
- console.log(this.name + ',' + this.age);
- }
- });
- // 添加靜態成員
- Person.extend({
- count: 0,
- addPerson: function () {
- this.count += 1;
- },
- getCount: function () {
- console.log('Person count: ' + this.count);
- }
- });
- // 建立一個Person類的實例
- var mark = new Person('Mark', 23);
- mark.log();
- // 訪問Person類的靜態方法
- Person.addPerson();
- Person.getCount(); // returns: 1
我如果想在傳送給類的構造函數的對象中爲類添加靜態成員呢?我們建立一個簡單的Mutator就可以了,來看下面代碼:
- Class.Mutators.Static = function (items) {
- this.extend(items);
- };
- var Person = new Class({
- // 添加靜態成員
- Static: {
- count: 0,
- addPerson: function () {
- this.count += 1;
- },
- getCount: function () {
- console.log('Person count: ' + this.count);
- }
- },
- initialize: function (name, age) {
- this.name = name;
- this.age = age;
- },
- log: function () {
- console.log(this.name + ',' + this.age);
- }
- });
- // 建立一個Person類的實例
- var mark = new Person('Mark', 18);
- mark.log();
- // 訪問Person類的靜態方法
- Person.addPerson();
- 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下實現屬性封裝是這樣實現的,看代碼示例:
- var Person = new Class({
- $name: '',
- $age: 0,
- $occupation: '',
- setName: function (name) {
- this.$name = name;
- return this;
- },
- getName: function () {
- return this.$name;
- },
- setAge: function (age) {
- this.$age = age;
- return this;
- },
- getAge: function () {
- return this.$age;
- },
- setOccupation: function (occupation) {
- this.$occupation = occupation;
- return this;
- },
- getOccupation: function () {
- return this.$occupation;
- }
- });
- var mark = new Person();
- mark.setName('Mark');
- mark.setAge(23);
- mark.setOccupation('JavaScript Developer');
- console.log(mark.getName() + ', ' + mark.getAge() + ': ' + mark.getOccupation());
- // 'Mark, 23: JavaScript Developer'
通過這種方式來實現封裝,如果屬性少的話還好,屬性一多你的代碼長度就客觀了,呵呵,在《Pro JavaScript with MooTools》一書中介紹了一個巧妙的Mutator,減少我們書寫的代碼量:
- Class.Mutators.GetterSetter = function (properties) {
- var klass = this; // 緩存this對象,這裏指向的是Class本身,如果不緩存會怎樣?$%#&^@#......
- Array.from(properties).each(function (property) {
- var captProp = property.capitalize(), // 把要添加的屬性名第一個字母變爲大寫
- $prop = '$' + property; // 爲屬性名添加'$'標識符,表明這個屬性爲私有變量
- // setter method
- klass.implement('set' + captProp, function (value) {
- this[$prop] = value;
- return this;
- });
- // getter method
- klass.implement('get' + captProp, function (value) {
- return this[$prop];
- });
- });
- };
- var Person = new Class({
- GetterSetter: ['name', 'age', 'occupation']
- });
- var mark = new Person();
- mark.setName('Mark');
- mark.setAge(23);
- mark.setOccupation('JavaScript Developer');
- console.log(mark.getName() + ', ' + mark.getAge() + ': ' + mark.getOccupation());
- // 'Mark, 23: JavaScript Developer'
看看設計一個類實現同樣的功能,使用GetterSetter Mutator是不是減少很多代碼呢。等等,這時有看官可能會說了,我們爲什麼要對屬性進行封裝呢,如果只是實現上面的功能直接爲Class添加name、age、occupation三個屬性,然後直接對它們進行訪問不就得了。嗯吶,這個嘛,上面的代碼的確體現不出封裝的好處。如果我們需要對類的某個私有變量進行一些邏輯控制,比如在setter方法中對值進行校驗、屬性值改變後我們需要觸發一個事件來應對值的改變等功能,這樣上面的GetterSetter Mutator就不能勝任了,我們就必須對他進行擴展,還好在MooTools Forge中我找到了這樣一個插件Class.Attributes,已經提供了這些功能,這樣我們只需要'拿來主義'就可以了,呵呵。這個源代碼進行了稍稍的修改:
- Class.Mutators.Attributes = function (attributes) {
- var $setter = attributes.$setter,
- $getter = attributes.$getter;
- delete attributes.$setter;
- delete attributes.$getter;
- this.implement({
- /**
- * @property $attributes
- * @description storage for instance attributes
- */
- $attributes: attributes,
- /**
- * @method get
- * @param name {String} - attribute name
- * @description attribute getter
- */
- get: function (name) {
- var attr = this.$attributes[name];
- if (attr) {
- // valueFn()對屬性的值進行初始化,不需要任何參數
- if (attr.valueFn && !attr.initialized) {
- attr.initialized = true;
- attr.value = attr.valueFn.call(this);
- }
- // 優先執行屬性中定義的getter方法來獲取屬性的值
- if (attr.getter) {
- return attr.getter.call(this, attr.value);
- } else {
- return attr.value;
- }
- } else {
- // 如果get的屬性沒有定義,則通過$getter方法來取得屬性值
- // $getter方法通過使用name的值if或switch擴展更多的屬性,不過這些屬性的readOnly,validator等方法需要自己在$getter中定義
- return $getter ? $getter.call(this, name) : undefined;
- }
- },
- /**
- * @method set
- * @param name {String} - attribute name
- * @param value {Object} - attribute value
- * @description attribute setter
- */
- set: function (name, value) {
- var attr = this.$attributes[name];
- if (attr) {
- // 首先判斷是不是隻讀屬性
- if (!attr.readOnly) {
- // 先緩存舊的屬性值
- var oldVal = attr.value, newVal;
- // 判斷屬性的校驗函數存不存在,如果存在則通過校驗函數確認可不可以賦值
- if (!attr.validator || attr.validator.call(this, value)) {
- // 優先使用屬性中定義的setter方法爲屬性賦值
- if (attr.setter) {
- newVal = attr.setter.call(this, value);
- } else {
- newVal = value;
- }
- attr.value = newVal;
- // #region - Extended by 苦苦的苦瓜 -
- /**
- # 如果新舊屬性值一樣,則不觸發事件
- **/
- if (oldVal !== value) {
- // 觸發自定義事件
- this.fireEvent(name + 'Change', { newVal: newVal, oldVal: oldVal });
- }
- // #endregion
- }
- }
- } else if ($setter) {
- // 如果屬性沒有定義
- if ($setter) { $setter.call(this, name, value); }
- }
- },
- /**
- * @method setAttributes
- * @param attributes {Object} - a list of attributes to be set to the instance
- * @description set passed attributes passing it through .set method
- */
- setAttributes: function (attributes) {
- Object.each(attributes, function (value, name) {
- this.set(name, value);
- }, this);
- },
- /**
- * @method getAttributes
- * @description returns a key-value object of all instance attributes
- * @returns {Object}
- */
- getAttributes: function () {
- var attributes = Object.clone(this.$attributes);
- return attributes;
- },
- /**
- * @method addAttributes
- * @param attributes {Object} - a list of new attributes to be added to the instance
- * @description adds list of attributes to the instance
- */
- addAttributes: function (attributes) {
- Object.each(attributes, function (value, name) {
- this.addAttribute(name, value);
- }, this);
- },
- /**
- * @method addAttribute
- * @param name {String} - new attribute name
- * @param value {Object} - new attribute value
- * @description adds new attribute to the instance
- */
- // 這裏的value屬性是一個對象,可以包含下面這些屬性方法value, valueFn(),getter(), setter(), readOnly, validator()
- addAttribute: function (name, value) {
- var attr = this.$attributes[name];
- // #region - Extended by 苦苦的苦瓜 -
- /**
- # 如果屬性已經存在則不覆蓋前屬性
- **/
- if (!attr) {
- attr = value;
- }
- // #endregion
- return this;
- }
- });
- };
功能多多,好處多多,呵呵,下面是示例代碼:
- var Product = new Class({
- Implements: [Options, Events],
- Attributes: {
- hmkhan: {
- valueFn: function () {
- return 'my name is HmKhan';
- }
- },
- brand: {
- validator: function (val) {
- return val.trim().length > 1;
- }
- },
- model: {
- validator: function (val) {
- return val.trim().length > 1;
- }
- },
- name: {
- readOnly: true,
- getter: function () {
- return this.get('brand') + ' ' + this.get('model');
- }
- },
- price: {
- getter: function (val) {
- return val * (100 - this.get('discount')) / 100
- }
- },
- discount: {
- value: 0 // Default value
- }
- },
- initialize: function (attributes) {
- this.setAttributes(attributes);
- }
- });
- var product = new Product({
- brand: 'Porsche',
- model: '911',
- price: 100000,
- discount: 5
- });
- console.log(product.get('name'));
- console.log(product.get('price'));
- product.addEvent('discountChange', function (event) {
- console.log("New discount: {newVal}% instead of {oldVal}%!".substitute(event));
- });
- product.set('discount', 30); // -> alerts "New discount: 30% instead of 5!"
- console.log(product.get('discount'));
- product.addAttribute('model', { value: '110' });
- console.log(product.get('model'));
- console.log(product.get('hmkhan'));
Binds Mutator
MooTools More提供了一個Mutator:Binds,用來綁定類的方法的作用域(至於爲什麼要綁定方法的作用域還有mootools中bind方法介紹這裏略過不說了,你懂的......)。來看下它的源代碼,很簡單:
- Class.Mutators.Binds = function (binds) {
- // 如果傳送給新類的構造函數的對象中沒有定義initialize方法,則爲新類定義一個空的initialize方法。
- if (!this.prototype.initialize) {
- this.implement('initialize', function () { });
- }
- // 把定義的Binds鍵值與類中已定義的Binds(繼承自父類)屬性值合併爲一個數組
- return Array.from(binds).concat(this.prototype.Binds || []);
- };
- // 絕妙的構思,把initialize定義爲一個Mutator,這裏傳遞過來的參數就是類構造函數中的initialize(初始化)方法
- Class.Mutators.initialize = function (initialize) {
- // 返回閉包作爲類的initialize方法
- return function () {
- // 綁定Binds屬性中包含的每個方法的作用域,替代原方法
- Array.from(this.Binds).each(function (name) {
- var original = this[name];
- if (original) { this[name] = original.bind(this); }
- }, this);
- // 執行類設計時構造函數中定義的initialize方法
- return initialize.apply(this, arguments);
- };
- };
爲什麼要把它拿出來單獨說一下呢,大家看看代碼,其實它是由兩個Mutators組成的:Binds和initialize。Binds Mutator把需要綁定作用域的方法名稱保存到類的Binds屬性中;initialize Mutator就耐人尋味了,我們定義一個新類時一般需要爲它定義一個initialize(初始化)方法,那麼他們兩個是什麼關係呢?還記得前面我們所說的“會檢查傳送給新類的構造函數的對象的每一個鍵,在Class.Mutators對象中是不是有mutator函數的對應的名字在裏面。如果找到了,它就調用這個函數並且把鍵的值傳給它做處理”,所以這時類構造函數中的initialize方法就作爲參數傳遞給initialize Mutator,initialize Mutator所做的工作就是生成並返回一個閉包,這個閉包執行的操作先是綁定Binds屬性中存儲的方法的作用域,然後再執行最初定義的initialize方法,MooTools會把這個返回的閉包在賦給initialize成員,實際上就是重寫構造函數的對象中定義的initialize方法。先看看下面的示例代碼:
- var MyClass = new Class({
- Binds: ['say'],
- initialize: function (element, message) {
- this.el = $(element);
- this.message = message;
- },
- monitor: function () {
- this.el.addEvent('click', this.say); //say is already bound to 'this'
- },
- stopMonitoring: function () {
- this.el.removeEvent('click', this.say);
- },
- say: function () {
- alert(this.message);
- }
- });
- var my = new MyClass('btnSay', 'this is a test.');
- 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