JavaScript之自定义类型
1、直接创建模式。这是最简单也是最直接的一种模式,首先创建一个引用类型的对象,然后为其添加自定义属性和方法。示例代码如下:
1 var person = new Object();
2 person.name = "Sam";
3 person.age = 16;
4 person.speak = function(){
5 alert(this.name + "is " + this.age + "years old");
6 }
7 person.speak();
可以看到,上面创建了一个Object类型的对象,然后为其添加了name和age属性以及一个speak方法。直接创建模式虽然简单,但其缺点是显而易见的:当我们需要创建许多相同的对象时,每次都要重复编写代码。为了解决这个问题,我们可以将创建对象的过程进行封装,于是便有了下面的工厂模式。
2、工厂模式。工厂模式是程序设计中一种常用的设计模式,它主要是将创建对象的过程进行了封装,示例代码如下:
1 function createPerson(name, age){
2 var person = new Object();
3 person.name = name;
4 person.age = age;
5 person.speak = function(){
6 alert(this.name + "is " + this.age + "years old");
7 }
8 return person;
9 }
10 var person1 = createPerson("Sam", 16);
11 var person2 = createPerson("Jack", 18);
使用工厂模式后,创建相同类型的对象变得简单了。但工厂模式没有解决对象识别的问题,即我们无法确定创建的对象的具体类型。有过面向对象编程经验的开发人员都知道,对象的创建应当基于类,有了具体的自定义类,再来创建该类的对象。幸好,在JavaScript中,我们可以通过构造函数模式来模拟一个类。
3、构造函数模式。构造函数和普通函数没有任何区别。任何普通函数都可以作为构造函数,只要使用new操作符即可;任何构造函数也都可以作为普通函数来调用。只不过在JavaScript中,有一个约定,就是用作构造函数的函数名需要首字母大写。示例代码如下:
1 function Person(name, age){
2 this.name = name;
3 this.age = age;
4 this.speak = function(){
5 alert(this.name + "is " + this.age + "years old");
6 }
7 }
8 var person1 = new Person("Sam", 16);
9 var person2 = new Person("Jack", 18);
可以看到,在构造函数内部,我们使用了this来添加属性和方法,那么,这个this是指什么呢?当我们创建了一个Person的对象时,this即是指这个创建的对象。现在,我们可以识别出对象person1和person2的具体类型了。使用alert(person1 instanceOf Person)后可以发现,输出的值为true。但构造函数模式也有自己的缺点,就是构造函数内声明的方法在每次创建新对象时都会重新创建(在JavaScript中,函数也是对象)。也就是说,构造函数内的方法是与对象绑定的,而不是与类绑定的。下面代码的输出可以验证我们的推断。
1 alert(person1.speak == person2.speak); // false
解决这个缺点的一种比较简单的方法就是将函数的声明放到构造函数的外面,即:
1 function Person(name, age){
2 this.name = name;
3 this.age = age;
4 this.speak = speak;
5 }
6 function speak(){
7 alert(this.name + "is " + this.age + "years old");
8 }
9 var person1 = new Person("Sam", 16);
10 var person2 = new Person("Jack", 18);
11 alert(person1.speak == person2.speak); // true
问题解决了,但这种方法又带来了新的问题。首先,函数speak是在全局作用域中声明的,但它却只能被用于Person构造函数,放在全局作用域中有被误用的风险;其次,如果一个自定义类型有很多的方法,则需要声明很多的全局函数,这既将导致全局作用域的污染,也不利于代码的封装。那么,有没有什么办法能让自定义类型的方法成为与类绑定的,又不污染全局作用域呢?答案是使用原型模式。
4、原型模式。在我们声明一个新的函数后,该函数(在JavaScript中,函数也是对象)就会拥有一个prototype的属性。prototype是一个对象,表示会被该函数创建的所有对象拥有的公共属性和方法。示例代码如下:
1 function Person(){}
2 Person.prototype.name="Sam";
3 Person.prototype.age=16;
4 Person.prototype.speak = function(){
5 alert(this.name + "is " + this.age + "years old");
6 }
7 var person1 = new Person();
8 person1.speak();
9 var person2 = new Person();
10 alert(person1.speak == person2.speak); // true
可以看到,虽然构造函数内没有声明speak方法,但我们创建的对象person1还是能调用speak方法,这是因为JavaScript有一个搜索规则,先搜索实例属性和方法,找到则返回;如果没找到,则再到prototype中去搜索。原型模式使得方法是与类相关的,并且没有污染全局作用域,但其也有自身的缺点:一是所有属性也都与类相关,这意味着所有对象共享一份属性,这显然是不合理的;二是没有办法向构造函数传入初始化数据了。解决的方法很简单,就是混合使用构造函数模式和原型模式。
5、组合模式。示例代码如下:
1 function Person(name, age){
2 this.name = name;
3 this.age = age;
4 }
5 Person.prototype.speak = function(){
6 alert(this.name + "is " + this.age + "years old");
7 }
8 var person1 = new Person();
9 person1.speak();
10 var person2 = new Person();
11 alert(person1.speak == person2.speak); // true
不难发现,组合模式实现了我们的所有需求,这也是目前应用得比较广泛的一种模式。有面向对象编程经验的开发人员可能会觉得将prototype的声明放在构造函数外面有点别扭,那么能否将其放到构造函数里去呢?答案是肯定的,使用动态组合模式即可。
6、动态组合模式。其原理就是先判断原型中的某个属性或方法是不是已经声明过,如果没有声明,则声明整个原型;否则,什么也不用做。示例代码如下:
1 function Person(name, age){
2 this.name = name;
3 this.age = age;
4 if (Person.prototype.speak == "undefined"){
5 Person.prototype.speak = function(){
6 alert(this.name + "is " + this.age + "years old");
7 }
8 }
9 }