10、类或对象的6种定义方法

可以以六种方式定义类或对象:工厂方式、构造函数方式、原型方式、混合的构造函数/原型方式、动态原型方法、混合工厂方式 ,下面看看个体每种方式。

  • 工厂方式

var oCar = new Object;
oCar.color = "red";
oCar.doors = 4;
oCar.mpg = 23;
oCar.showColor = function () {
	alert(this.color);
};

 

上面的创建方式有一点不太好就是的,如果要创建多个car对象时,这时我们就会要重复写上面的代码,指定color、doors、mpg与 showColor属性方法。要解决此问题,开发者创造了能创建并返回特定类型的对象的工厂函数(factory function)。例如,函数createCar()可用于封装前面列出的创建car对象的操作:

function createCar(color, doors, mpg) {
	var oTempCar = new Object;
	oTempCar.color = color;
	oTempCar.doors = doors;
	oTempCar.mpg = mpg;
	oTempCar.showColor = function () {
		alert(this.color + " " + this.doors + " " + this.mpg);
	};
	return oTempCar;
}
var oCar1 = createCar("red", 4, 23);
var oCar1 = createCar("blue", 3, 25);

 

以上方式存在功能问题:功能问题在于用这种方式必须创建对象的方法。上面的例子中,每次调用函数createCar(),都要创建新函数 showColor(),意味着每个对象都有自己的showColor()版本,事实上,每个对象都是可以共享同一个函数。但下面可以避开此种问题,请看重写后的代码:

function showColor() {
	alert(this.color + " " + this.doors + " " + this.mpg);
}
function createCar(color, doors, mpg) {
	var oTempCar = new Object;
	oTempCar.color = color;
	oTempCar.doors = doors;
	oTempCar.mpg = mpg;
	oTempCar.showColor = showColor;
	return oTempCar;
} 
  • 构造函数方式  

function Car(color, doors, mpg) {
	this.color = color;
	this.doors = doors;
	this.mpg = mpg;
	this.showColor = function () {
		alert(this.color + " " + this.doors + " " + this.mpg);
	};
}
var oCar1 = new Car("red", 4, 23);
var oCar2 = new Car("blue", 3, 25);
oCar1.showColor();
oCar2.showColor();

你可能已经注意到第一个差别了,在构造函数内部无创建对象,而是使用this关键字。使用new运算符调用构造函数时,在执行第一行代码前就先创建出一个对象,然后才是执行构造函数,这与Java是一样的。在对象里面也只有用this才能访问该对象。this默认情况下是构造函数的返回值(不必明确使用 return运算符)
现在,用new运算符和类名Car创建对象,就更像创建ECMAScript中一般对象了。你也许会问,这种方式在管理函数方面是否存在与前一种方式相同的问题呢?是的。就像工厂函数,构造函数会重复生成函数,为每个对象都创建独立的函数版本。不过,与工厂函数相似,也可以用外部函数重写构造函数,同样的,语义上无任何意义。这就是原型方式的优势所在。 

  • 原型方式

该方式利用了对象的prototype属性,可把它看成创建新对象所依赖的原型。这里,用空构造函数来设置类名。然后所有的属性和方法都被直接赋予prototype属性。重新前面的例子,代码如下所示:

function Car() {
}
Car.prototype.color = "red";
Car.prototype.doors = 4;
Car.prototype.mpg = 23;
Car.prototype.showColor = function () {
	alert(this.color + " " + this.doors + " " + this.mpg);
 };
 var oCar1 = new Car();
 var oCar2 = new Car();
 oCar1.showColor();
 oCar2.showColor();

在这段代码中,首先定义构造函数(Car),其中无任何代码。接下来的几行代码,通过给Car的prototype属性添加属性定义Car对象的属性。调用new Car()时,原型的所有属性都被立即赋予要创建的对象,意味着所有Car实例存放的都是指向showColor()函数的指针。从语义上讲,所有属性看起来都属于一个对象,因此解决了前面两种方式的两个问题。

这个看起来是个非常好的解决方案。遗憾的是,并非尽如人意。
首先,这个构造函数没有参数。使用原型方式时,不能通过给构造函数传递参数初始化属性的值,因为car1和car2的color属性都等于"red",doors属性都等于4,mpg属性都等于23。这意味必须在对象创建后才能改变属性的默认值,这点很令人讨厌,但这还不是真真的问题所在。真正的问题出现在属性如果指向的是一个对象,而不是函数时。函数共享不会造成任何问题,因为代码区永远是可以共享的,但对象一般却不是我们所需要的多个实例共享的它。如果在上面代码的基础上为原型加如下属性时:

Car.prototype.Drivers = new Array('Mike','Sue');
oCar1.drivers.push('Matt');
alert(oCar1.drivers);//Mike,Sue,Matt
alert(oCar2.drivers); //Mike,Sue,Matt 

 由于drivers是引用值,Car的两个实例都指向同一个数组。这意味着给car1.drivers添加值"Matt",在car2.drivers中也能看到。
上面创建对象有很多的问题,下面联合使用构造函数和原型方式来解决这些问题。

  • 混合的构造函数/原型方式

用构造函数定义对象的所有非函数属性,用原型方式定义对象的函数属性(方法)。结果所有函数都只创建一次,而每个对象都具有自己的对象属性实例。再重写前面的例子,代码如下:

function Car(color, doors, mpg) {
	this.color = color;
	this.doors = doors;
	this.mpg = mpg;
	this.drivers = new Array("Mike", "Sue");
}
Car.prototype.showColor = function () {
	alert(this.color + " " + this.doors + " " + this.mpg);
};
var oCar1 = new Car("red", 4, 23);
var oCar2 = new Car("blue", 3, 25);
oCar1.drivers.push("Matt");
alert(oCar1.drivers);//Mike,Sue,Matt
alert(oCar2.drivers);//Mike,Sue 

现在就更像创建一般对象了。所有的非函数属性都在构造函数中创建。因为只创建showColor()函数的一个实例,所以没有内存浪费。这种方式是 ECMAScript主要采用的方式,它具有其他方式的特性,却没有它们的副作用。不过,有些开发者仍觉得这种方法不够完美。

  • 动态原型方法

批评混合的构造函数/原型方式的人认为,面向对象的设计就要求把属性与方法封装在类里面,而在其外部定义方法的做法不合逻辑。因此,他们设计了动态原型方法,以提供像面向对象语言一样的更友好的编码风格。
动态原型方法的基本想法与混合的构造函数/原型方式相同,即在构造函数内定义非函数属性,而函数属性则利用原型属性定义。唯一的区别是赋予对象方法的位置,把函数属性的定义放置到了构造函数里。下面是用动态原型方法重写的Car类:

function Car(color, doors, mpg) {
	this.color = color;
	this.doors = doors;
	this.mpg = mpg;
	this.drivers = new Array("Mike", "Sue");
	if (typeof Car._initialized == "undefined") {
		Car.prototype.showColor = function () {
			alert(this.color + " " + this.doors + " " + this.mpg);
		};
		Car._initialized = true;
	}
}

该方法使用标志(_initialized)来判断是否已给原型赋予了任何方法。该方法只创建并赋值一次,为取悦传统的OOP开发者,这段代码看起来更像其他语言中的类定义了。

  • 混合工厂方式

创建假构造函数,只返回另一种对象的新实例。这段代码看来与工厂函数非常相似:

function Car(color, doors, mpg) {
	var oTempCar = new Object;
	oTempCar.color = color;
	oTempCar.doors = doors;
	oTempCar.mpg = mpg;
	oTempCar.showColor = function () {
		alert(this.color + " " + this.doors + " " + this.mpg);
	};
	return oTempCar;
}

 

与经典方式不同,这种方式使用new运算符,使它看起来像真正的构造函数:

var car = new Car(); 

 

由于在Car()构造函数内部调用了new运算符,所以将忽略赋值表达中的new运算符(位于构造函数之外)(注:如果返回的是一个基本类型,则会忽略方法里的return,具体原理请见《7、对象》——《构造函数》 一节) 。在构造函数内部创建的对象被传递回变量car。这种方式在对象方法的内部管理方面与经典方式有着相同的问题。建议还是避免使用这种方式。

  • 采用哪种方式

如前所述,目前使用最广泛的是混合的构造函数/原型方式。此外,动态原型方法也很流行,在功能上与构造函数/原型方式等价。可以采用这两种方式中的任何一种。不过不要单独使用经典的构造函数或原型方式,因为这样会给代码引入问题。

你可能感兴趣的