首先需要说明的是,本文将直接讲解创建和使用类的各种技巧,一些基础的东西不再做解释,要理解如何在JavaScript中实现面向对象的设计,请先参考《JavaScript.高级程序设计(第2版)》(前7章)、《javascript.设计模式》(前四章)、《JavaScript.语言精粹》这三部经典之作。
在Mootools中使用Class构造函数创建一个类有两种方式,也就是传递两种不同类型的参数给构造函数,第一种也是标准方式传递一个对象字面量,这个对象字面量可以包括你为类添加的所有属性、方法。例如:
1 var Person = new Class({
2 // Methods
3 initialize: function (name, age) {
4 this.name = name;
5 this.age = age;
6 },
7
8 log: function () {
9 console.log(this.name + ',' + this.age);
10 }
11 });
12
13 var mark = new Person('mark', 24);
14 mark.log(); // returns 'mark,24'
第二种是传递一个普通函数,mootools会自动把这个函数包装成只含一个initialize键值的对象字面量,然后你可以使用implement方法对类进行扩展,例如:
1 var Person = new Class(function (name, age) {
2 this.name = name;
3 this.age = age;
4 });
5 Person.implement('log', function () {
6 console.log(this.name + ',' + this.age);
7 });
8 var mark = new Person('mark', 24);
9 mark.log(); // returns 'mark,24'
当然推荐使用的还是第一种方式,直观明了嘛。你如果使用标准方式建立了一个类,也是可以使用implement方法对类进行扩展的,如果你真的认为有必要把一个类的设计拆成几个部分的话(例如在使用掺元对象实现多亲继承时,神马?等等...这个MooTools里实现多亲继承继承不是使用Implements Mutator吗,嗯呐,这个在接下来类的继承中将详细讲解......),呵呵,至于MooTools内部如果对构造函数进行解析,有兴趣的可看看MooTools 1.4 源码分析 - Class 修正版。
Implement and Extend
Implement方法用来为类添加新的方法、属性。需要注意的是,如果新添加的方法或属性与类中旧有的方法或属性同名,则会覆盖类中旧有的方法、属性。调用Implement方法有两种方式,第一种方式传递两个参数,第一个参数为String类型,存储要添加的方法或属性的名称,第二个参数为方法所对应的函数或属性所对应的值,这种方式每次只能为类添加一个方法或属性:
1 Person.implement('log', function () {
2 console.log(this.name + ',' + this.age);
3 });
4 Person.implement('city', '深圳');
第二种方式传递一个对象字面量参数,把要添加的方法属性包含在这个对象中,一次添加多个方法、属性,避免重复调用implement:
1 Person.implement({
2 'city': '深圳',
3 'log': function () {
4 console.log(this.name + ',' + this.age);
5 }
6 });
MooTools关于Class的官方文档中只暴露了implement一个方法,其实对类本身进行操作的还有一个比较重要的方法extend,这个方法之所以没有出现在Class的文档中。这是因为它不是作为Class的特殊方法,而实际上是Type的方法。它的作用是为类创建静态成员,静态成员关联的是类本身,换句话说,静态成员是在类的层次上操作,而不是在实例的层次上操作,每个静态成员都只有一份。调用extend方法的方式同Implement,也是两种方式。
简单一点讲,implement为实例创建方法和属性,extend为类本身创建方法和变量,请看下面的例子:
1 var Person = new Class(function (name, age) {
2 this.name = name;
3 this.age = age;
4 });
5 Person.implement({
6 instanceMethod: function () {
7 console.log('From an instance!');
8 }
9 });
10 Person.extend({
11 classMethod: function () {
12 console.log('From the class itself!');
13 }
14 });
15
16 var mark = new Person('mark', 24);
17
18 console.log(typeOf(mark.instanceMethod)); // returns 'function'
19 mark.instanceMethod(); // returns 'From an instance!'
20 console.log(typeOf(mark.classMethod)); // returns 'null',说明实例是不能调用静态方法的
21
22 console.log(typeOf(Person.classMethod)); // returns 'function'
23 Person.classMethod(); // returns 'From the class itself!'
24 console.log(typeOf(Person.instanceMethod)); // returns 'null',同样类也不能直接调用为实例而创建的方法
25
26 Person.prototype.instanceMethod(); // 类只能通过这种方式调用原型上的方法
私有成员
严格来讲,JavaScript中没有私有成员的概念,所有对象的属性都是共有的。不过,倒是有一个私有变量的概念,任何在函数中定义的变量,都可以认为是私有变量,因为不能在函数的外部访问浙西变量。私有变量包括函数的参数、局部变量和在函数内定义的其他函数。所以我们可以通过使用闭包来为类制造私有成员:
1 var Person = (function () {
2 // 私有变量
3 var numOfPersons = 0;
4
5 // 私有方法
6 var formatName = function (name) {
7 return name.capitalize();
8 };
9
10 return new Class({
11 initialize: function (name, age) {
12 this.name = name;
13 this.age = age;
14 numOfPersons++;
15 },
16
17 // 公有方法
18 log: function () {
19 console.log(formatName(this.name) + ',' + this.age);
20 },
21
22 getNumOfPersons: function () {
23 return numOfPersons;
24 }
25 });
26 })();
27
28 var mark = new Person('mark', 24);
29 mark.log(); // returns 'mark,24'
30 console.log(mark.getNumOfPersons()); // returns 1
使用这个模式有一个好处就是,私有成员在内存中只会存放一份,是由所有实例共享的,不必为每一个实例生成一个副本。但这也延伸出一个问题,来看下面的代码:
1 var Person = (function () {
2 // 私有变量
3 var name = '';
4
5 return new Class({
6 initialize: function (v1, v2) {
7 name = v1;
8 this.age = v2;
9 },
10
11 getName: function () {
12 return name;
13 },
14
15 setName: function (value) {
16 name = value;
17 },
18
19 getAge: function () {
20 return this.age;
21 },
22
23 setAge: function (value) {
24 this.age = value;
25 }
26 });
27 })();
28
29 var mark = new Person('mark', 24);
30 console.log(mark.getName()); // 'mark'
31 mark.setName('grey');
32 console.log(mark.getName()); // 'grey'
33 console.log(mark.getAge()); // 24
34
35 var john = new Person('john', 18);
36 console.log(john.getName()); // 'john'
37 console.log(john.getAge()); // 18
38 console.log(mark.getName()); // 'john'
39 console.log(mark.getAge()); // 24
这个例子中的Person构造函数(这里指initialize)与getName()和setName()方法一样,都有权访问私有变量name,在这种模式下,变量name就变成了一个静态的、有所有实例共享的属性,也就是说,在一个实例上调用setName()会影响所有实例,结果就是所有实例getName()都会返回相同的值,而age是实例变量就不存在这个问题。到底是使用实例变量还是静态私有变量,最终还是要视你的需求而定。
当然上面这个问题只是针对私有变量的,私有方法就不存在这个问题,相比实例方法会更有效率(从内存占用的意义上来说),应为它只会被创建一份。
使用闭包还带来一个问题,多查找作用域链中的一个层次,就会在一定程度上影响查找的速度(一般情况可以忽略不计),鱼与熊掌不可兼得啊......
常量
最简单设置常量的方法是为类添加一个静态属性,然而静态属性是公有的,类的使用者可以随时改变它的值,这个样的操作后果是很严重的。这里我们可以使用前面介绍的为类设置静态私有变量的方式来模拟常量,然后在实例方法中只创建取值器方法而不创建赋值器方法。这样类的使用者只能使用暴露出来的取值器方法来得到私有变量的值而不能改变它的值。来看下面的代码:
1 var Person = (function () {
2 // 私有变量
3 var AGE_UPPER_BOUND = 32;
4
5 return new Class({
6 initialize: function (v1, v2) {
7 // ...
8 },
9
10 getAGEUPPERBOUND: function (value) {
11 return AGE_UPPER_BOUND;
12 }
13 });
14 })();
如果需要使用多个常量,设置一个私有的对象字面量来存储这些常量,然后设置一个通用的取值器方法来取得这些常量:
1 var Person = (function () {
2 // 私有变量
3 var constants = {
4 AGE_UPPER_BOUND: 32,
5 AGE_LOWER_BOUND: 18
6 };
7
8 return new Class({
9 initialize: function (v1, v2) {
10 // ...
11 },
12
13 getConstants: function (name) {
14 return constants[name];
15 }
16 });
17 })();
继承
继承的主要好处表现在代码的重用方面,通过建立类之间的继承关系,有些方法我们只需要定义一次就可以了。同样,如果需要修改这些方法或排查其中的错误,那么由于其定义只出现在一个位置,所以非常有利于节省时间和精力。MooTools实现类的继承有两种模式:原型式继承和多亲继承,原型式继承由内建的Extends Mutator来实现,多亲继承由Implements Mutator或implement方法来实现。
原型式继承这里就不多讲了,着重讲一下多亲继承。在JavaScript里,因为一个对象只能拥有一个原型对象,所以不允许子类继承多个超类,不过我们可以利用多个掺元类(minix class)或掺元对象对一个类进行扩充,这样类的实例就可以拥有mixin类(对象)中的方法、属性,所以这实际上实现了多继承的效果。通常mixin类(对象)包含一些通用的方法,大家可以看看MooTools里Class.Extras模块中三个mixin类的定义(Chain、Events、Options)。
这里还需要注意的的一点是,在派生具有私有成员的的类或implement mixin类(对象)时,因为在父类(或mixin)中访问这些私有变量的方法是公有的,所以他们也会被遗传下来。所以子类可以间接访问父类(或mixin)的私有成员,但子类自身的实例方法都不能直接访问这些私有属性,而且你也不能在子类中添加能够直接访问他们的方法(作用域变量,你想访问也访问不了啦,呵呵)。
首先我们先建立一个基类:
1 var Animal = new Class({
2 initialize: function (age) {
3 this.age = age;
4 }
5 });
使用Extends Mutator派生一个子类:
1 var Cat = new Class({
2 Extends: Animal,
3 initialize: function (name, age) {
4 this.parent(age); // calls initalize method of Animal class
5 this.name = name;
6 }
7 });
8
9 var cat = new Cat('Micia', 20);
10 console.log(cat.name); // 'Micia'
11 console.log(cat.age); // 20
利用Implements Mutator扩充一个类,首先建立一个mixin类:
1 var Mixin = new Class({
2 getName: function () {
3 return this.name;
4 },
5 setName: function (value) {
6 this.name = value
7 }
8 });
9
10 var Cat = new Class({
11 Extends: Animal,
12 Implements: Mixin,
13 initialize: function (name, age) {
14 this.parent(age); // calls initalize method of Animal class
15 this.name = name;
16 }
17 });
18
19 var cat = new Cat('Micia', 20);
20 console.log(cat.name); // 'Micia'
21 console.log(cat.age); // 20
22 cat.setName('Dog');
23 console.log(cat.getName()); // 'Dog'
使用implement方法扩充一个类,首先家里一个mixin对象:
1 // mixin对象存储一些通用方法,可以被不同的类implement
2 var objMixin = (function () {
3 var counter = 0;
4
5 return {
6 init: function () {
7 counter += 1;
8 },
9 getCounter: function () {
10 return counter;
11 },
12 getAge: function () {
13 return this.age;
14 },
15 setAge: function (value) {
16 this.age = value;
17 }
18 };
19 })();
20
21 var Cat = new Class({
22 Extends: Animal,
23 Implements: Mixin,
24 initialize: function (name, age) {
25 this.parent(age); // calls initalize method of Animal class
26 this.name = name;
27 }
28 });
29 Cat.implement(objMixin);
30
31 var Dog = new Class({
32 Extends: Animal,
33 Implements: Mixin,
34 initialize: function (name, age) {
35 this.parent(age); // calls initalize method of Animal class
36 this.name = name;
37 }
38 });
39 Dog.implement(objMixin);
40
41 var cat = new Cat('Micia', 20);
42 console.log(cat.name); // 'Micia'
43 console.log(cat.age); // 20
44 cat.setName('汤姆');
45 console.log(cat.getName()); // '汤姆'
46 cat.setAge(12);
47 console.log(cat.getAge()); // 12
48 // 对mixin对象的私有属性进行操作
49 cat.init();
50 console.log(cat.getCounter()); // 1
51
52 var dog = new Dog('小狗', 6);
53 console.log(dog.name); // '小狗'
54 console.log(dog.age); // 6
55 dog.setName('布鲁托');
56 console.log(dog.getName()); // '布鲁托'
57 dog.setAge(8);
58 console.log(cat.getAge()); // 8
59 // 对mixin对象的私有属性进行操作
60 dog.init();
61 console.log(dog.getCounter()); // 2
62 console.log(cat.getCounter()); // 2
大家都看明白了吧,呵呵,不过通过上面的代码我们引申出另外一个问题,注意上面的Cat类的设计,我们首先设计了Extends,然后是Implements,再就是Cat类本身的方法属性,MooTools内部对Class构造函数解析时是按照我们设计时的顺序解析的吗?答案是按照我们设计时的顺序解释的。简单来讲MooTools通过for-in对对象进行枚举来遍历每个成员进行解释的,等等......那个ECMAScript最新版对for-in 语句的遍历机制又做了调整,属性遍历的顺序是没有被规定的,也就是说随机的,那么MooTools是怎样保证按顺序解释的呢?先看下面这段代码:
1 var obj = {
2 Waa: "Waa",
3 aa: 'aa',
4 68: '68',
5 15: '15',
6 tt: 'tt',
7 '-7': '-7',
8 _: "___",
9 online: true
10 };
11 for (var k in obj) {
12 console.log(k);
13 }
把它放在各个浏览器都执行一遍,你会发现IE、火狐、Safari浏览器的JavaScript 解析引擎遵循的是较老的ECMA-262第三版规范,属性遍历顺序由属性构建的顺序决定,而Chrome、Opera中使用 for-in 语句遍历对象属性时会遵循一个规律,它们会先提取所有 key 的 parseFloat 值为非负整数的属性, 然后根据数字顺序对属性排序首先遍历出来,然后按照对象定义的顺序遍历余下的所有属性。其它浏览器则完全按照对象定义的顺序遍历属性。
这下明白了吧,只要你为类设计的方法、属性还有Mutator的名称不为数字就可以了(当然如果你非要有这样的嗜好,我也只能@#%&$......)。请看下面的代码:
1 var Super = new Class({
2 log: function () {
3 console.log('Super');
4 }
5 });
6
7 var Mixin = new Class({
8 log: function () {
9 console.log('Mixin');
10 }
11 });
12
13 var Sub = new Class({
14 Extends: Super,
15 Implements: Mixin
16 });
17
18 var obj = new Sub();
19 obj.log(); // ?
在这里obj.log()会返回什么呢?对了是'Maxin',这里Sub类首先继承了Super类,Sub的原型实际就是Super类的一个实例,Super的log方法也就是成了Sub的原型上的一个方法,然后执行Implements Mutator 为Sub类的原型扩展了一个Mixin类的实例上的方法,这时Mixin类实例上的log方法就覆盖了Sub类原型上原来的log方法(继承自Super类)。
如果把Extends、Implements的顺序颠倒一下:
1 var Sub = new Class({
2 Implements: Mixin,
3 Extends: Super
4 });
5
6 var obj = new Sub();
7 obj.log(); // ?
这时obj.log()会返回什么呢?还是'Maxin'吗?其实这里返回的是'Super',Why?前面我们介绍了MooTools对Class构造函数解析时是按照我们设计的顺序解析的,所以在这里首先执行的是Implements Mutator,它首先为Sub类的原型扩展了一个Mixin类的实例上的log方法,然后才是对超类Super的继承,因为在JavaScrpt里每个对象只有一个原型,原型式继承的原理就是超类的一个实例赋予子类的原型,子类原来的原型这时会被超类的实例替换掉,所以这是Sub类原型的引用已经指向了超类的实例,而他自己的原型对象这时被消除了,所以之前从Mixin类得来的那个log方法,对不起跟着一起魂飞湮灭了,所以这里返回的是'Super'。
当然如果你嫌不过瘾,那就在为Sub类添加一个log方法:
1 var Sub = new Class({
2 Implements: Mixin,
3 Extends: Super,
4
5 log: function () {
6 console.log('sub');
7 }
8 });
9 var obj = new Sub();
10 obj.log(); // ?
你可以把Sub类的Implements、Extends、log来回颠倒一下看看效果,呵呵,再用implement方法在扩展一个试试:
1 var objMixin = {
2 log: function () {
3 console.log('objMixin');
4 }
5 };
6
7 var Sub = new Class({
8 Implements: Mixin,
9 Extends: Super,
10
11 log: function () {
12 console.log('sub');
13 }
14 });
15 Sub.implement(objMixin);
16
17 var obj = new Sub();
18 obj.log(); // ?
呵呵,别晕掉,一切都是为了把问题搞的跟明白不是......
最后不要忘记两个重要的方法:parent()和protect(),这里就不多说了,在前面的Class源码分析里有详细介绍。
下一篇在详细讲解一下Mutators。
苦苦的苦瓜 - 2011-10-07