未分类

JavaScript-OOP常见模式总结

一、创建对象模式

1. 工厂模式

使用一个函数作为工场函数,封装以特定接口创建对象的细节,每次调用工场函数都能生产一个对象。工厂模式的缺点是无法解决对象识别问题(即知道一个对象的类型),而且每次调用函数,都会创建一个带有属性和方法的对象,也就是说,一些共同的方法会被多次创建,即每个方法都会在每个对象上重新创建一遍。以下面的代码为例,obj1obj2都分别有各自的setProperty方法,也就是说setProperty方法创建了两次,浪费资源空间。

1
2
3
4
5
6
7
8
9
10
11
12
13
function createObj(property1, property2) {
var o = new Object();
o.property1 = property1;
o.property2 = property2;
o.setProperty = function(val1, vla2) {
this.property1 = val1;
this.property2 = val2;
};
return o;
}

var obj1 = createObj('xxxx', 'xxxx');
var obj2 = createObj('yyyy', 'yyyy');

2. 构造函数模式

通过创建自定义的构造函数从而定义自定义对象类型的属性和方法。构造函数内没有显式地创建对象,直接将属性和方法赋值给this对象,也没有return语句。在使用new操作符创建对象的时候,会经历以下过程:创建一个新对象,把构造函数的作用域赋值给新对象(即this指向新对象),执行构造函数中的代码给对象添加属性方法,最后返回新对象的引用,可以结合下面代码中obj3的创建过程理解整个过程。使用这种模式创建的对象,可以用instanceof操作符来判定对象类型,这是相对于工厂模式的优势。但是,这种模式也会出现相同的方法多次创建的情况,像下面的代码一样,setProperty方法被创建了3次,obj1obj2obj3都有各自的一个setProperty方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
function MyObject(property1, property2) {
this.property1 = property1;
this.property2 = property2;
this.setProperty = function(val1, val2) {
this.property1 = val1;
this.property2 = val2;
};
}

var obj1 = new MyObject('xxxx', 'xxxx');
var obj2 = new MyObject('yyyy', 'yyyy');
var obj3 = new Object();
MyObject.call(obj3, 'zzzz', 'zzzz');

3. 原型模式

我们创建的每个函数都有一个prototype(原型)属性,该属性是一个指针,指向一个对象。创建自定义一个新函数之后,JavaScript会根据一组特定规则为该函数创建一个prototype属性,指向函数的原型对象。原型对象有一个constructor属性,指向prototype属性所属的函数指针。例如:MyObject.prototype指向MyObject的原型对象,MyObject.prototype.constructor指回MyObject。原型模式的代码示例如下所示。通过原型模式定义的属性和方法将被所有对象实例共享,以下面代码为例,obj1obj2property属性是共享的,没有各自创建自己的property,当原型对象上property属性值修改之后obj1obj2访问到的值也跟着变了。代码执行读取某个对象的属性时,会执行一次搜索,首先从对象实例本身开始,如果找到该属性则返回该属性的值,如果找不到,则通过prototype指针继续在原型对象上进行搜索。基于这种搜索机制,所有创建在原型对象上的属性都是被共享的,因为只要在对象实例上找不到就会到原型对象上找,而对象实例有多个,它们的prototype指针指向的原型对象却都是同一个。当然,只要在对象实例上继续创建属性,该属性与原型对象的属性同名,则可以屏蔽原型对象上的属性。原型模式的缺点是所有属性和方法都共享,这点对于函数非常合适(类似于c++中类的成员函数),当对于某些属性来说很不合适(类似于c++中类的非静态成员变量)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function MyObject() {}
MyObject.prototype.property = 'xxxx';
MyObject.prototype.getProperty = function () {
return this.property;
};
MyObject.prototype.setProperty = function(val) {
this.property = val
};

var obj1 = new MyObject();
var obj2 = new MyObject();
console.log(obj1.getProperty()); // 输出"xxxx"
console.log(obj2.getProperty()); // 输出"xxxx"
MyObject.prototype.property = 'yyyy';
console.log(obj1.getProperty()); // 输出"yyyy"
console.log(obj2.getProperty()); // 输出"yyyy"

4. 组合模式(构造函数模式+原型模式)

结合构造函数模式和原型模式的优点,克服它们的缺点。这种模式下,在构造函数中定义对象各自的私有属性,而共享方法则在原型对象上定义。这样,每个对象实例都有各自的属性变量,而共享原型对象上的函数方法,很好地模拟了类的公有函数和私有变量。这种模式是JavaScript中使用最广泛的一种创建自定义类型的模式。以下面代码为例,obj1obj2有各自的property属性变量,当修改原型对象上的property属性值时对obj1obj2没有影响,当obj1property属性值改变时对obj2也没有影响。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function MyObject(property) {
this.property = property;
}

MyObject.prototype.getProperty = function() {
return this.property;
};

var obj1 = new MyObject('xxxx');
var obj2 = new MyObject('yyyy');
console.log(obj1.getProperty()); // 输出"xxxx"
console.log(obj2.getProperty()); // 输出"yyyy"
MyObject.prototype.property = 'zzzz';
console.log(obj1.getProperty()); // 输出"xxxx"
console.log(obj2.getProperty()); // 输出"yyyy"
obj1.property = 'zzzz';
console.log(obj1.getProperty()); // 输出"zzzz"
console.log(obj2.getProperty()); // 输出"yyyy"

二、继承模式

1. 原型链模式

原型链是实现继承的主要方法,利用原型链让一个引用类型继承另一个引用类型的属性和方法。每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个指向原型对象的内部指针。当我们让原型对象等于另一个类型的实例,此时原型对象将包含一个指向另一个原型的指针,相应地,另一个原型中也包含着一个指向另一个构造函数的指针。假如另一个原型有事另一个类型的实例,那么上述关系依然成立,如此层层递进就构成了实例与原型的链条,这就是原型链的基本概念。实现的本质是重写原型对象,替换为一个新类型的实例。以下面代码为例,SubType的原型对象变成SuperType的一个实例对象之后,为了叙述方便,我们将此对象称为sobj,也就是说现在SubType的原型对象是sobjsobjSuperType的一个实例对象。当我调用getSuperName()方法时候,首先在obj对象上进行搜索,找不到getSuperName方法,接着在obj的原型对象sobj上寻找,也是没找到,而sobj本身也有prototype指针指向SuperType的原型对象,所以会继续在这个原型对象上找,找到了getSuperName方法,搜索停止,执行getSuperName方法。通过实现原型链,本质上扩展了原型搜索机制。原型链实现继承也会存在不少缺点,首先,父类型作为子类型的原型对象,我们前面说过,原型对象上的属性和方法会被所有对象共享,于是子类型会共享所有父类型的属性和方法,这就导致了父类型的属性无法当做私有属性来使用。其次,在创建子类型的实例时,无法向父类型的构造函数传参。基于这两点,在实际应用中很少单独使用原型链。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function SuperType() {
this.superName = 'SuperType';
}
SuperType.prototype.getSuperName = function() {
return this.superName;
};

function SubType() {
this.subName = 'SubType';
}
SubType.prototype = new SuperType(); // 改变子类的prototype指针指向,实现继承
SubType.prototype.getSubName = function() {
return this.subName;
};

var obj = new SubType();
console.log(obj.getSuperName()); // 输出"SuperType"
console.log(obj.getSubName()); // 输出"SubType"

2. 借用构造函数模式

借用构造函数模式的基本思想相当简单,即在子类型构造函数的内部调用超类型的构造函数。通过使用apply()call()方法在新创建的对象上执行构造函数,从而将父类构造函数执行作用域切换到子类,为子类定义一系列父类的属性方法,如下所示,在创建SubType的实例对象时,在该对象上调用了父类的构造函数,因此会执行父类构造函数中的this.superName = 'SuperType'这句代码,此时的this指向的时子类的实例对象obj,所以obj上就有了superName属性。

1
2
3
4
5
6
7
8
9
10
11
12
function SuperType() {
this.superName = 'SuperType';
}

function SubType() {
SuperType.call(this); // 在子类对象上调用父类构造函数
this.subName = 'SubType';
}

var obj = new SubType();
console.log(obj.superName); // 输出"SuperType"
console.log(obj.subName); // 输出"SubType"

借用构造函数有一个很大的优势,可以在子类型中向超类型构造函数传参。但此技术也存在缺点,即所有超类型的函数方法必须在构造函数中定义才能被子类继承,而在构造函数中定义函数方法,就会出现前面说的构造函数模式所遇到的问题,这些方法会被多次创建,即在每一个实例对象上都会被创建一遍。因此,借用构造函数模式也很少单独使用。

3. 组合继承模式

组合继承也叫作伪经典继承,它将原型链和借用构造函数的技术组合起来,发挥二者的优势。使用原型链实现对原型属性和方法的继承,通过借用构造函数实现对实例属性的继承,这样,既通过在原型上定义的方法实现了函数复用,又能保证每个实例都有自己的属性。组合继承模式避免了原型链和借用构造函数的缺陷,融合了他们的优点,是JavaScript中最常用的继承模式,而且,instanceofisPrototypeOf()也能够用于识别基于组合继承创建的对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function SuperType(superName) {
this.superName = superName;
}
SuperType.prototype.saySuperName = function() {
console.log(this.superName);
};

function SubType(superName, subName) {
SuperType.call(this, superName); // 使用借用构造函数模式继承父类属性
this.subName = subName;
}
SubType.prototype = new SuperType(); // 使用原型链模式继承父类方法
SubType.prototype.constructor = SubType; // 重置构造子
SubType.prototype.saySubName = function() {
console.log(this.subName);
};

var obj = new SubType('heheheheh', '233333');
obj.saySuperName(); // 输出"heheheheh"
obj.saySubName(); // 输出"233333"

分享到