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