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