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时不能使用对象字面量的方式建立,看看下面代码:
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