可以以六种方式定义类或对象:工厂方式、构造函数方式、原型方式、混合的构造函数/原型方式、动态原型方法、混合工厂方式 ,下面看看个体每种方式。
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。这种方式在对象方法的内部管理方面与经典方式有着相同的问题。建议还是避免使用这种方式。
如前所述,目前使用最广泛的是混合的构造函数/原型方式。此外,动态原型方法也很流行,在功能上与构造函数/原型方式等价。可以采用这两种方式中的任何一种。不过不要单独使用经典的构造函数或原型方式,因为这样会给代码引入问题。