Javascript自定义类或对象

来源:互联网 发布:淘宝义卖 编辑:程序博客网 时间:2024/05/16 00:26

工厂方式

由于对象的属性可在对象创建后动态定义,所以许多开发者都在初次引入JavaScript时编写类似下面的代码:

在这段代码中,创建对象car。然后给它设置几个属性:它的颜色是红色,有四个门,每加仑油23英里。最后一个属性实际上是指向函数的指针,意味着该属性是个方法。执行这段代码后,就可以使用对象car。问题是可能需要创建多个car实例。

要解决此问题,开发者创造了能创建并返回特定类型的对象的工厂函数factory function)。例如,函数createCar()可用于封装前面列出的创建car对象的操作:

这里,前面的所有代码都包含在createCar()函数中,此外还有一行额外的代码,返回car对象作为(oTempCar)函数值。调用此函数时,将创建新对象,并赋予它所有必要的属性,复制出一个前面说明的car对象。使用该方法,可以容易地创建car对象的两个版本(oCar1oCar2),它们的属性完全一样。当然,还可以修改createCar()函数,给它传递各个属性的默认值,而不是赋予属性默认值:

createCar()函数加上参数,即可为要创建的car对象的colordoorsmpg属性赋值。这使两个对象具有相同的属性,却有不同的属性值。

虽然ECMAScript越来越正式化,创建对象的方法却被置之不理,且其规范化至今还遭人反对。一部分是语义上的原因(它看起来不像使用带有构造函数的new运算符那么正规),一部分是功能上的原因。功能问题在于用这种方式必须创建对象的方法。前面的例子中,每次调用函数createCar(),都要创建新函数showColor(),意味着每个对象都有自己的showColor()版本,事实上,每个对象都共享了同一个函数。

有些开发者在工厂函数外定义对象的方法,然后通过属性指向该方法,从而避开这个问题:

在这段重写的代码中,在函数createCar()前定义了函数showColor()。在createCar()内部,赋予对象一个指向已经存在的showColor()函数的指针。从功能上讲,这样解决了重复创建函数对象的问题,但该函数看起来不像对象的方法。

所有这些问题引发了开发者定义的构造函数的出现。

3.5.2  构造函数方式

创建构造函数就像定义工厂函数一样容易。第一步选择类名,即构造函数的名字。根据惯例,这个名字的首字母大写,以使它与首字母通常是小写的变量名区分开。除了这点不同,构造函数看起来很像工厂函数。考虑下面的例子:

你可能已经注意到第一个差别了,在构造函数内部无创建对象,而是使用this关键字。使用new运算符调用构造函数时,在执行第一行代码前先创建一个对象,只有用this才能访问该对象。然后可以直接赋予this属性,默认情况下是构造函数的返回值(不必明确使用return运算符)。

现在,用new运算符和类名car创建对象,就更像创建ECMAScript中一般对象了。你也许会问,这种方式在管理函数方面是否存在与前一种方式相同的问题呢?是的。

就像工厂函数,构造函数会重复生成函数,为每个对象都创建独立的函数版本。不过,与工厂函数相似,也可以用外部函数重写构造函数,同样的,语义上无任何意义。这就是原型方式的优势所在。

 

原型方式

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

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

看起来是个非常好的解决方案。遗憾的是,并非尽如人意。

首先,这个构造函数没有参数。使用原型方式时,不能通过给构造函数传递参数初始化属性的值,因为car1car2color属性都等于"red"doors属性都等于4mpg属性都等于23。这意味必须在对象创建后才能改变属性的默认值,这点很令人讨厌,但还不至于是世界末日。真正的问题出现在属性指向的是对象,而不是函数时。函数共享不会造成任何问题,但对象却很少被多个实例共享的。考虑下面的例子:

这里,属性drivers是指向Array对象的指针,该数组中包含两个名字"Mike""Sue"。由于drivers是引用值,Car的两个实例都指向同一个数组。这意味着给car1.drivers添加值"Matt",在car2.drivers中也能看到。输出这两个指针中的任何一个,结果都是显示字符串"Mike,Sue,Matt"

由于创建对象时有这么多问题,你一定会想,是否有种合理的创建对象的方法呢?答案是联合使用构造函数和原型方式。

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

联合使用构造函数和原型方式,就可像用其他程序设计语言一样创建对象。这种概念非常简单,即用构造函数定义对象的所有非函数属性,用原型方式定义对象的函数属性(方法)。结果所有函数都只创建一次,而每个对象都具有自己的对象属性实例。再重写前面的例子,代码如下:

现在就更像创建一般对象了。所有的非函数属性都在构造函数中创建,意味着又可用构造函数的参数赋予属性默认值了。因为只创建showColor()函数的一个实例,所以没有内存浪费。此外,给oCar1drivers数组添加"Matt"值,不会影响oCar2的数组,所以输出这些数组的值时,oCar1.drivers显示的是"Mike,Sue,Matt",而oCar2.drivers显示的是"Mike,Sue"。由于使用了原型方式,所以仍然能利用instanceof运算符判断对象的类型。

这种方式是ECMAScript主要采用的方式,它具有其他方式的特性,却没有它们的副作用。不过,有些开发者仍觉得这种方法不够完美。

3.5.5  动态原型方法

对于习惯使用其他语言的开发者来说,使用混合的构造函数/原型方式感觉不那么和谐。毕竟,定义类时,大多数面向对象语言都对属性和方法进行了视觉上的封装。考虑下面的Java类:

Java很好的打包了Car类的所有属性和方法,因此看见这段代码就知道它要实现什么功能,它定义了一个对象的信息。批评混合的构造函数/原型方式的人认为,在构造函数内存找属性,在其外部找方法的做法不合逻辑。因此,他们设计了动态原型方法,以提供更友好的编码风格。

动态原型方法的基本想法与混合的构造函数/原型方式相同,即在构造函数内定义非函数属性,而函数属性则利用原型属性定义。唯一的区别是赋予对象方法的位置。下面是用动态原型方法重写的Car类:

直到检查typeof Car._initialized是否等于"undefined"之前,这个构造函数都未发生变化。这行代码是动态原型方法中最重要的部分。如果这个值未定义,构造函数将用原型方式继续定义对象的方法,然后把Car._initialized设置为true。如果这个值定义了(它的值为true时,typeof的值为Boolean),那么就不再创建该方法。简而言之,该方法使用标志(_initialized)来判断是否已给原型赋予了任何方法。该方法只创建并赋值一次,为取悦传统的OOP开发者,这段代码看起来更像其他语言中的类定义了。

3.5.6  混合工厂方式

这种方式通常是在不能应用前一种方式时的变通方法。它的目的是创建假构造函数,只返回另一种对象的新实例。这段代码看来与工厂函数非常相似:

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

由于在Car()构造函数内部调用了new运算符,所以将忽略第二个new运算符(位于构造函数之外)。在构造函数内部创建的对象被传递回变量var

这种方式在对象方法的内部管理方面与经典方式有着相同的问题。强烈建议:除非万不得已(请参阅第15章),还是避免使用这种方式。

3.5.7  采用哪种方式

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

3.5.8  实例

对象令人感兴趣的一点是用它们解决问题的方式。ECMAScript中最常见的一个问题是字符串连接的性能。与其他语言类似,ECMAScript的字符串是不可变的,即它们的值不能改变。考虑下面的代码:

实际上,这段代码在幕后执行的步骤如下:

(1) 创建存储"hello"的字符串。

(2) 创建存储"world"的字符串。

(3) 创建存储连接结果的字符串。

(4) str的当前内容复制到结果中。

(5) "world"复制到结果中。

(6) 更新str,使它指向结果。

每次完成字符串连接都会执行步骤26,使得这种操作非常消耗资源。如果重复这一过程几百次,甚至几千次,就会造成性能问题。解决方法是用Array对象存储字符串,然后用join()方法(参数是空字符串)创建最后的字符串。想像用下面的代码代替前面的代码:

这样,无论在数组中引入多少字符串都不成问题,因为只在调用join()方法时才会发生连接操作。此时,执行的步骤如下:

(1) 创建存储结果的字符串。

(2) 把每个字符串复制到结果中的合适位置。

虽然这种解决方法很好,但还有更好的方法。问题是这段代码不能确切反映出它的意图。要使它更容易理解,可以用StringBuffer类打包该功能:

第一点要注意的是,这段代码是strings属性,本意是私有属性。它只有两个方法,即append()toString()方法。append()方法有一个参数,它把该参数附加到字符串数组中,toString()方法调用数组的join()方法,返回真正连接成的字符串。要用StringBuffer对象连接一组字符串,可以用下面的代码:

可用下面代码测试StringBuffer对象和传统的字符串连接方法的性能:

这段代码对字符串连接进行两个测试,第一个使用加号,第二个使用StringBuffer类。每个操作都连接10000个字符串。日期值d1d2用于判断完成操作需要的时间。记住,创建新Date对象时,如果没有参数,赋予对象的是当前的日期与时间。要计算连接操作历经多少时间,把日期的毫秒表示(getTime()方法的返回值)相减即可。这是衡量JavaScript性能的常用方法。该测试的结果应该说明使用StringBuffer类比使用加号节省了100~200%的时间。

原创粉丝点击