js如何定义类或对象

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。
这种方式在对象方法的内部管理方面与经典方式有着相同的问题。强烈建议:除非万不得已(请参阅第15章),还是避免使用这种方式。
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对象和传统的字符串连接方法的性能:
这段代码对字符串连接进行两个测试,第一个使用加号,第二个使用 StringBuffer类。每个操作都连接10000个字符串。日期值 d1和 d2用于判断完成操作需要的时间。记住,创建新 Date对象时,如果没有参数,赋予对象的是当前的日期与时间。要计算连接操作历经多少时间,把日期的毫秒表示( getTime()方法的返回值)相减即可。这是衡量JavaScript性能的常用方法。该测试的结果应该说明使用 StringBuffer类比使用加号节省了100%~200%的时间。
创建对象只是使用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的实例并能立即使用(接下来的一行)。
不建议使用极晚绑定方法,因为很难对其跟踪和记录。不过,还是应该了解这种可能。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值