js如何定义类或对象
2006-12-28 16:28
3.5.3 原型方式
该方式利用了对象的
prototype属性,可把它看成创建新对象所依赖的原型。这里,用空构造函数来设置类名。然后所有的属性和方法都被直接赋予
prototype属性。重新前面的例子,代码如下所示:
![]()
在这段代码中,首先定义构造函数(
Car),其中无任何代码。接下来的几行代码,通过给
Car的
prototype属性添加属性定义
Car对象的属性。调用
new Car()时,原型的所有属性都被立即赋予要创建的对象,意味着所有
Car实例存放的都是指向
showColor()函数的指针。从语义上讲,所有属性看起来都属于一个对象,因此解决了前面两种方式的两个问题。此外,使用该方法,还能用
instanceof运算符检查给定变量指向的对象的类型。因此,下面的代码将输出
true:
![]()
看起来是个非常好的解决方案。遗憾的是,并非尽如人意。
首先,这个构造函数没有参数。使用原型方式时,不能通过给构造函数传递参数初始化属性的值,因为
car1和
car2的
color属性都等于"
red",
doors属性都等于
4,
mpg属性都等于
23。这意味必须在对象创建后才能改变属性的默认值,这点很令人讨厌,但还不至于是世界末日。真正的问题出现在属性指向的是对象,而不是函数时。函数共享不会造成任何问题,但对象却很少被多个实例共享的。考虑下面的例子:
![]() ![]() ![]()
这里,属性
drivers是指向
Array对象的指针,该数组中包含两个名字"
Mike"和"
Sue"。由于
drivers是引用值,
Car的两个实例都指向同一个数组。这意味着给
car1.drivers添加值"
Matt",在
car2.drivers中也能看到。输出这两个指针中的任何一个,结果都是显示字符串"
Mike,Sue,Matt"。
由于创建对象时有这么多问题,你一定会想,是否有种合理的创建对象的方法呢?答案是联合使用构造函数和原型方式。
3.5.4 混合的构造函数/原型方式
联合使用构造函数和原型方式,就可像用其他程序设计语言一样创建对象。这种概念非常简单,即用构造函数定义对象的所有非函数属性,用原型方式定义对象的函数属性(方法)。结果所有函数都只创建一次,而每个对象都具有自己的对象属性实例。再重写前面的例子,代码如下:
![]() ![]()
现在就更像创建一般对象了。所有的非函数属性都在构造函数中创建,意味着又可用构造函数的参数赋予属性默认值了。因为只创建
showColor()函数的一个实例,所以没有内存浪费。此外,给
oCar1的
drivers数组添加"
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。
![]()
3.5.7 采用哪种方式
如前所述,目前使用最广泛的是混合的构造函数/原型方式。此外,动态原型方法也很流行,在功能上与构造函数/原型方式等价。可以采用这两种方式中的任何一种。不过不要单独使用经典的构造函数或原型方式,因为这样会给代码引入问题。
3.5.8 实例
对象令人感兴趣的一点是用它们解决问题的方式。ECMAScript中最常见的一个问题是字符串连接的性能。与其他语言类似,ECMAScript的字符串是不可变的,即它们的值不能改变。考虑下面的代码:
![]()
实际上,这段代码在幕后执行的步骤如下:
(1) 创建存储"
hello"的字符串。
(2) 创建存储"
world"的字符串。
(3) 创建存储连接结果的字符串。
(4) 把
str的当前内容复制到结果中。
(5) 把"
world"复制到结果中。
(6) 更新
str,使它指向结果。
每次完成字符串连接都会执行步骤2到6,使得这种操作非常消耗资源。如果重复这一过程几百次,甚至几千次,就会造成性能问题。解决方法是用
Array对象存储字符串,然后用
join()方法(参数是空字符串)创建最后的字符串。想像用下面的代码代替前面的代码:
![]()
这样,无论在数组中引入多少字符串都不成问题,因为只在调用
join()方法时才会发生连接操作。此时,执行的步骤如下:
(1) 创建存储结果的字符串。
(2) 把每个字符串复制到结果中的合适位置。
虽然这种解决方法很好,但还有更好的方法。问题是这段代码不能确切反映出它的意图。要使它更容易理解,可以用
StringBuffer类打包该功能:
![]() ![]() ![]()
第一点要注意的是,这段代码是
strings
的属性,本意是私有属性。它只有两个方法,即
append()和
toString()方法。
append()方法有一个参数,它把该参数附加到字符串数组中,
toString()方法调用数组的
join()方法,返回真正连接成的字符串。要用
StringBuffer对象连接一组字符串,可以用下面的代码:
![]()
可用下面代码测试
StringBuffer对象和传统的字符串连接方法的性能:
![]() ![]()
创建对象只是使用ECMAScript的乐趣的一部分。你喜欢修改已有对象的行为吗?这在ECMAScript中是完全可能的,所以可为
String、
Array、
Number或其他任意一种对象设计出你想要的任何方法,因为有无限的可能性。
还记得本章前面的小节中介绍的
prototype属性吗?你已经知道,每个构造函数都有个
prototype属性,可用于定义方法。你还不知道的是,在ECMAScript中,每个本地对象也有个用法完全相同的
prototype属性。
3.6.1 创建新方法
可以用
prototype属性为任何已有的类定义新方法,就像处理自己的类一样。例如,还记得
Number类的
toString()方法吗,如果给它传递16,它将输出十六进制的字符串。难道用
toHexString()方法处理这个操作不是更好吗?创建它很简单:
![]()
在此环境中,关键字
this指向
Number的实例,因此可完全访问
Number的所有方法。有了这段代码,可实现下面操作:
![]()
由于数字15等于十六进制中的F,因此警告将显示"
F"。还记得将数组用作队列的讨论吗?唯一漏掉的是命名正确的方法。可以给
Array类添加两个方法
enqueue()和
dequeue(),只让它们反复调用已有的
push()和
shift()方法即可:
![]()
当然,还可添加与已有方法无关的方法。例如,假设要判断某个项在数组中的位置,没有本地方法可以做这种事情。则可以轻松地创建下面的方法:
![]() ![]()
![]()
该方法
indexOf()与
String类的同名方法保持一致,在数组中检索每个项,直到发现与传进来的项相等的项为止。如果找到相等的项,则返回该项的位置,否则,返回
-1。使用这种定义,可以采用下面的代码:
![]()
最后,如果想给ECMAScript中的每个本地对象添加新方法,必须在
Object对象的
prototype属性上定义它。如上一章所述,所有本地对象都继承了
Object对象,所以对
Object对象做任何改变,都会反应在所有本地对象中。例如,如果想添加一个用警告输出对象的当前值的方法,可以采用下面的代码:
![]()
这里,
String和
Number对象都从
Object对象继承了
showValue()方法,分别在它们的对象上调用该方法,将显示"
hello"和"
25"。
3.6.2 重定义已有方法
就像能给已有的类定义新方法一样,也可重定义已有的方法。如前一章所述,函数名只是指向函数的指针,因此可以轻易地使它指向其他函数。如果修改了本地方法,如
toString(),会出现什么情况呢?
![]()
前面的代码完全合法,运行结果完全符合预期:
![]() ![]()
也许你还记得,第2章中介绍过
Function的
toString()方法通常输出的是函数的源代码。覆盖该方法,可以返回另一个字符串(在这个例子中,返回"
Function code hidden")。不过,
toString()指向的原始函数怎样了呢?它将被无用存储单元回收程序回收,因为它被完全废弃了。没能够恢复原始函数的办法,所以在覆盖原始方法前,存储它的指针比较安全,以便以后的使用。你甚至可能在某种情况下在新方法中调用原始方法:
![]()
在这段代码中,第一行代码把对当前
toString()方法的引用保存在属性
originalTo- String中。然后用定制的方法覆盖了
toString()方法。新方法将检查该函数源代码的长度是否大于100。如果是,就返回错误消息,说明该函数代码太长,否则调用
originalToString()方法,返回函数的源代码。
3.6.3 极晚绑定
从技术上来说,根本不存在极晚绑定。本书采用该术语描述ECMAScript中的一种现象,即能够在对象实例化后再定义它的方法。例如:
![]()
在大多数程序设计语言中,必须在实例化对象之前定义对象的方法。这里,方法
sayHi()是在创建
Object类的一个实例后才添加进来的。在传统语言中不仅没听说过这种操作,也没听说过该方法还会自动赋予
Object的实例并能立即使用(接下来的一行)。
不建议使用极晚绑定方法,因为很难对其跟踪和记录。不过,还是应该了解这种可能。
|