JavaScript
中支持面向对象的基础
6.1.1
用定义函数的方式定义类
在面向对象的思想中,最核心的概念之一就是类。一个类表示了具有相似性质的一类事物的抽象,通过实例化一个类,可以获得属于该类的一个实例,即对象。
在
JavaScript
中定义一个类的方法如下:
function class1(){
//
类成员的定义及构造函数
}
这里
class1
既是一个函数也是一个类。可以将它理解为类的构造函数,负责初始化工作。
6.1.2
使用
new
操作符获得一个类的实例
在前面介绍基本对象时,已经用过
new
操作符,例如:
new Date();
表示创建一个日期对象,而
Date
就是表示日期的类,只是这个类是由
JavaScript
内部提供的,而不是由用户定义的。
new
操作符不仅对内部类有效,对用户定义的类也同样有效,对于上节定义的
class1
,也可以用
new
来获取一个实例:
function class1(){
//
类成员的定义及构造函数
}
var obj1=new class1();
抛开类的概念,从代码的形式上来看,
class1
就是一个函数,那么是不是所有的函数都可以用
new
来操作呢?是的,在
JavaScript
中,函数和类就是一个概念,当对一个函数进行
new
操作时,就会返回一个对象。如果这个函数中没有初始化类成员,那就会返回一个空的对象。例如:
//
定义一个
hello
函数
function hello(){
alert("hello");
}
//
通过
new
一个函数获得一个对象
var obj=new hello();
alert(typeof(obj));
从运行结果看,执行了
hello
函数,同时
obj
也获得了一个对象的引用。当
new
一个函数时,这个函数就是所代表类的构造函数,其中的代码被看作为了初始化一个对象。用于表示类的函数也称为构造器。
6.1.3
使用方括号(
[ ]
)引用对象的属性和方法
在
JavaScript
中,每个对象可以看作是多个属性(方法)的集合,引用一个属性(方法)很简单,如:
对象名
.
属性(方法)名
还可以用方括号的形式来引用:
对象名
["
属性(方法)名
"]
注意,这里的方法名和属性名是一个字符串,不是原先点(
?
)号后面的标识符,例如:
var arr=new Array();
//
为数组添加一个元素
arr["push"]("abc");
//
获得数组的长度
var len=arr["length"];
//
输出数组的长度
alert(len);
图
6.1
显示了执行的结果。
由此可见,上面的代码等价于:
var arr=new Array();
//
为数组添加一个元素
arr.push("abc");
//
获得数组的长度
var len=arr.length;
//
输出数组的长度
alert(len);
这种引用属性(方法)的方式和数组类似,体现了
JavaScript
对象就是一组属性(方法)的集合这个性质。
这种用法适合不确定具体要引用哪个属性(方法)的情况,例如:一个对象用于表示用户资料,用一个字符串表示要使用的那个属性,就可以用这种方式来引用:
<script language="JavaScript" type="text/javascript">
<!--
//
定义了一个
User
类,包括两个成员
age
和
sex
,并指定了初始值。
function User(){
this.age=21;
this.sex="male";
}
//
创建
user
对象
var user=new User();
//
根据下拉列表框显示用户的信息
function show(slt){
if(slt.selectedIndex!=0){
alert(user[slt.value]);
}
}
//-->
</script>
<!--
下拉列表框用于选择用户信息
-->
<select onchange="show(this)">
<option>
请选择需要查看的信息:
</option>
<option value="age">
年龄
</option>
<option value="sex">
性别
</option>
</select>
在这段代码中,使用一个下拉列表框让用户选择查看哪个信息,每个选项的
value
就表示用户对象的属性名称。这时如果不采用方括号的形式,可使用如下代码来实现:
function show(slt){
if(slt.selectedIndex!=0){
if(slt.value=="age")alert(user.age);
if(slt.value=="sex")alert(user.sex);
}
}
而使用方括号语法,则只需写为:
alert(user[slt.value]);
方括号语法像一种参数语法,可用一个变量来表示引用对象的哪个属性。如果不采用这种方法,又不想用条件判断,可以使用
eval
函数:
alert(eval("user."+slt.value));
这里利用
eval
函数的性质,执行了一段动态生成的代码,并返回了结果。
实际上,在前面讲述
document
的集合对象时,就有类似方括号的用法,比如引用页面中一个名为
“theForm”
的表单对象,以前的用法是:
document.forms["theForm"];
也可以改写为:
document.forms.theForm;
forms
对象是一个内部对象,和自定义对象不同的是,它还可以用索引来引用其中的一个属性。
6.1.4
动态添加、修改、删除对象的属性和方法
上一节介绍了如何引用一个对象的属性和方法,现在介绍如何为一个对象添加、修改或者删除属性和方法。
其他语言中,对象一旦生成,就不可更改,要为一个对象添加、修改成员必须要在对应的类中修改,并重新实例化,程序也必须重新编译。
JavaScript
提供了灵活的机制来修改对象的行为,可以动态添加、修改、删除属性和方法。例如:先用类
Object
来创建一个空对象
user
:
var user=new Object();
1
.添加属性
这时
user
对象没有任何属性和方法,可以为它动态的添加属性,例如:
user.name="jack";
user.age=21;
user.sex="male";
通过上述语句,
user
对象具有了三个属性:
name
、
age
和
sex
。下面输出这三个语句:
alert(user.name);
alert(user.age);
alert(user.sex);
由代码运行效果可知,三个属性已经完全属于
user
对象了。
2
.添加方法
添加方法的过程和添加属性类似:
user.alert=function(){
alert("my name is:"+this.name);
}
这就为
user
对象添加了一个方法
“alert”
,通过执行它,弹出一个对话框显示自己的名字:
user.alert();
图
6.2
显示了执行的结果。
3
.修改属性和方法
修改一个属性和方法的过程就是用新的属性替换旧的属性,例如:
user.name="tom";
user.alert=function(){
alert("hello,"+this.name);
}
这样就修改了
user
对象
name
属性的值和
alert
方法,它从显示
“my name is”
对话框变为了显示
“hello”
对话框。
4
.删除属性和方法
删除一个属性和方法的过程也很简单,就是将其置为
undefined
:
user.name=undefined;
user.alert=undefined;
这样就删除了
name
属性和
alert
方法。
在添加、修改或者删除属性时,和引用属性相同,也可以采用方括号(
[]
)语法:
user["name"]="tom";
使用这种方式还有一个特点,可以使用非标识符字符串作为属性名称,例如标识符中不允许以数字开头或者出现空格,但在方括号(
[]
)语法中却可以使用:
user["my name"]="tom";
需要注意,在使用这种非标识符作为名称的属性时,仍然要用方括号语法来引用:
alert(user["my name"]);
而不能写为:
alert(user.my name);
事实上,
JavaScript
中的每个对象都是动态可变的,这给编程带来了灵活性,也和其他语言产生了区别。
6.1.5
使用大括号(
{ }
)语法创建无类型对象
传统的面向对象语言中,每个对象都会对应到一个类。上一节讲
this
指针时提到,
JavaScript
中的对象其实就是属性(方法)的一个集合,并没有严格意义上类的概念。所以它提供了一种简单的方式来创建对象,即大括号(
{}
)语法:
{
property1:statement,
property2:statement2,
…,
propertyN:statmentN
}
通过大括号括住多个属性或方法及其定义(这些属性或方法用逗号隔开),来实现对象的定义,这段代码就直接定义个了具有
n
个属性或方法的对象,其中属性名和其定义之间用冒号(
:
)隔开。例如:
<script language="JavaScript" type="text/javascript">
<!--
var obj={}; //
定义了一个空对象
var user={
name:"jack", //
定义了
name
属性,初始化为
jack
favoriteColor:["red","green","black","white"],//
定义了颜色喜好数组
hello:function(){ //
定义了方法
hello
alert("hello,"+this.name);
},
sex:"male" //
定义了性别属性
sex
,初始化为
male
}
//
调用
user
对象的方法
hello
user.hello();
//-->
</script>
第一行定义了一个无类型对象
obj
,它等价于:
var obj=new Object();
接着定义了一个对象
user
及其属性和方法。注意,除了最后一个属性(方法)定义,其他的必须以逗号(
,
)结尾。其实,使用动态增减属性的方法也可以定义一个完全相同的
user
对象,读者可使用前面介绍的方法实现。
使用这种方式来定义对象,还可以使用字符串作为属性(方法)名,例如:
var obj={"001":"abc"}
这就给对象
obj
定义了一个属性
“001”
,这并不是一个有效的标识符,所以要引用这个属性必须使用方括号语法:
obj["001"];
由此可见,无类型对象提供了一种创建对象的简便方式,它以紧凑和清晰的语法将一个对象体现为一个完整的实体。而且也有利于减少代码的体积,这对
JavaScript
代码来说尤其重要,减少体积意味着提高了访问速度。
6.1.6
prototype
原型对象
prototype
对象是实现面向对象的一个重要机制。每个函数(
function
)其实也是一个对象,它们对应的类是
“Function”
,但它们身份特殊,每个函数对象都具有一个子对象
prototype
。即
prototype
表示了该函数的原型,而函数也是类,
prototype
就是表示了一个类的成员的集合。当通过
new
来获取一个类的对象时,
prototype
对象的成员都会成为实例化对象的成员。
既然
prototype
是一个对象,可以使用前面两节介绍的方法对其进行动态的修改,这里先给出一个简单的例子:
//
定义了一个空类
function class1(){
//empty
}
//
对类的
prototype
对象进行修改,增加方法
method
class1.prototype.method=function(){
alert("it's a test method");
}
//
创建类
class1
的实例
var obj1=new class1();
//
调用
obj1
的方法
method
obj1.method();
图
6.3
显示了执行的结果。
6.2
深入认识
JavaScript
中的函数
6.2.1
概述
函数是进行模块化程序设计的基础,编写复杂的
Ajax
应用程序,必须对函数有更深入的了解。
JavaScript
中的函数不同于其他的语言,每个函数都是作为一个对象被维护和运行的。通过函数对象的性质,可以很方便的将一个函数赋值给一个变量或者将函数作为参数传递。在继续讲述之前,先看一下函数的使用语法:
function func1(…){…}
var func2=function(…){…};
var func3=function func4(…){…};
var func5=new Function();
这些都是声明函数的正确语法。它们和其他语言中常见的函数或之前介绍的函数定义方式有着很大的区别。那么在
JavaScript
中为什么能这么写?它所遵循的语法是什么呢?下面将介绍这些内容。
6.2.2
认识函数对象(
Function Object
)
可以用
function
关键字定义一个函数,并为每个函数指定一个函数名,通过函数名来进行调用。在
JavaScript
解释执行时,函数都是被维护为一个对象,这就是要介绍的函数对象(
Function
Object
)。
函数对象与其他用户所定义的对象有着本质的区别,这一类对象被称之为内部对象,例如日期对象(
Date
)、数组对象(
Array
)、字符串对象(
String
)都属于内部对象。这些内置对象的构造器是由
JavaScript
本身所定义的:通过执行
new Array()
这样的语句返回一个对象,
JavaScript
内部有一套机制来初始化返回的对象,而不是由用户来指定对象的构造方式。
在
JavaScript
中,函数对象对应的类型是
Function
,正如数组对象对应的类型是
Array
,日期对象对应的类型是
Date
一样,可以通过
new Function()
来创建一个函数对象,也可以通过
function
关键字来创建一个对象。为了便于理解,我们比较函数对象的创建和数组对象的创建。先看数组对象:下面两行代码都是创建一个数组对象
myArray
:
var myArray=[];
//
等价于
var myArray=new Array();
同样,下面的两段代码也都是创建一个函数
myFunction
:
function myFunction(a,b){
return a+b;
}
//
等价于
var myFunction=new Function("a","b","return
a+b");
通过和构造数组对象语句的比较,可以清楚的看到函数对象本质,前面介绍的函数声明是上述代码的第一种方式,而在解释器内部,当遇到这种语法时,就会自动构造一个
Function
对象,将函数作为一个内部的对象来存储和运行。从这里也可以看到,一个函数对象名称(函数变量)和一个普通变量名称具有同样的规范,都可以通过变量名来引用这个变量,但是函数变量名后面可以跟上括号和参数列表来进行函数调用。
用
new
Function()
的形式来创建一个函数不常见,因为一个函数体通常会有多条语句,如果将它们以一个字符串的形式作为参数传递,代码的可读性差。下面介绍一下其使用语法:
var funcName=new Function(p1,p2,...,pn,body);
参数的类型都是字符串,
p1
到
pn
表示所创建函数的参数名称列表,
body
表示所创建函数的函数体语句,
funcName
就是所创建函数的名称。可以不指定任何参数创建一个空函数,不指定
funcName
创建一个无名函数,当然那样的函数没有任何意义。
需要注意的是,
p1
到
pn
是参数名称的列表,即
p1
不仅能代表一个参数,它也可以是一个逗号隔开的参数列表,例如下面的定义是等价的:
new Function("a", "b", "c", "return
a+b+c")
new Function("a, b, c", "return a+b+c")
new Function("a,b", "c", "return a+b+c")
JavaScript
引入
Function
类型并提供
new Function()
这样的语法是因为函数对象添加属性和方法就必须借助于
Function
这个类型。
函数的本质是一个内部对象,由
JavaScript
解释器决定其运行方式。通过上述代码创建的函数,在程序中可以使用函数名进行调用。本节开头列出的函数定义问题也得到了解释。注意可直接在函数声明后面加上括号就表示创建完成后立即进行函数调用,例如:
var i=function (a,b){
return a+b;
}(1,2);
alert(i);
这段代码会显示变量
i
的值等于
3
。
i
是表示返回的值,而不是创建的函数,因为括号
“(”
比等号
“=”
有更高的优先级。这样的代码可能并不常用,但当用户想在很长的代码段中进行模块化设计或者想避免命名冲突,这是一个不错的解决办法。
需要注意的是,尽管下面两种创建函数的方法是等价的:
function funcName(){
//
函数体
}
//
等价于
var funcName=function(){
//
函数体
}
但前面一种方式创建的是有名函数,而后面是创建了一个无名函数,只是让一个变量指向了这个无名函数。在使用上仅有一点区别,就是:对于有名函数,它可以出现在调用之后再定义;而对于无名函数,它必须是在调用之前就已经定义。例如:
<script language="JavaScript" type="text/javascript">
<!--
func();
var func=function(){
alert(1)
}
//-->
</script>
这段语句将产生
func
未定义的错误,而:
<script language="JavaScript" type="text/javascript">
<!--
func();
function func(){
alert(1)
}
//-->
</script>
则能够正确执行,下面的语句也能正确执行:
<script language="JavaScript" type="text/javascript">
<!--
func();
var someFunc=function func(){
alert(1)
}
//-->
</script>
由此可见,尽管
JavaScript
是一门解释型的语言,但它会在函数调用时,检查整个代码中是否存在相应的函数定义,这个函数名只有是通过
function
funcName()
形式定义的才会有效,而不能是匿名函数。
6.2.3
函数对象和其他内部对象的关系
除了函数对象,还有很多内部对象,比如:
Object
、
Array
、
Date
、
RegExp
、
Math
、
Error
。这些名称实际上表示一个类型,可以通过
new
操作符返回一个对象。然而函数对象和其他对象不同,当用
typeof
得到一个函数对象的类型时,它仍然会返回字符串
“function”
,而
typeof
一个数组对象或其他的对象时,它会返回字符串
“object”
。下面的代码示例了
typeof
不同类型的情况:
alert(typeof(Function)));
alert(typeof(new Function()));
alert(typeof(Array));
alert(typeof(Object));
alert(typeof(new Array()));
alert(typeof(new Date()));
alert(typeof(new Object()));
运行这段代码可以发现:前面
4
条语句都会显示
“function”
,而后面
3
条语句则显示
“object”
,可见
new
一个
function
实际上是返回一个函数。这与其他的对象有很大的不同。其他的类型
Array
、
Object
等都会通过
new
操作符返回一个普通对象。尽管函数本身也是一个对象,但它与普通的对象还是有区别的,因为它同时也是对象构造器,也就是说,可以
new
一个函数来返回一个对象,这在前面已经介绍。所有
typeof
返回
“function”
的对象都是函数对象。也称这样的对象为构造器(
constructor
),因而,所有的构造器都是对象,但不是所有的对象都是构造器。
既然函数本身也是一个对象,它们的类型是
function
,联想到
C++
、
Java
等面向对象语言的类定义,可以猜测到
Function
类型的作用所在,那就是可以给函数对象本身定义一些方法和属性,借助于函数的
prototype
对象,可以很方便地修改和扩充
Function
类型的定义,例如下面扩展了函数类型
Function
,为其增加了
method1
方法,作用是弹出对话框显示
"function"
:
Function.prototype.method1=function(){
alert("function");
}
function func1(a,b,c){
return a+b+c;
}
func1.method1();
func1.method1.method1();
注意最后一个语句:
func1.method1.mehotd1()
,它调用了
method1
这个函数对象的
method1
方法。虽然看上去有点容易混淆,但仔细观察一下语法还是很明确的:这是一个递归的定义。因为
method1
本身也是一个函数,所以它同样具有函数对象的属性和方法,所有对
Function
类型的方法扩充都具有这样的递归性质。
Function
是所有函数对象的基础,而
Object
则是所有对象(包括函数对象)的基础。在
JavaScript
中,任何一个对象都是
Object
的实例,因此,可以修改
Object
这个类型来让所有的对象具有一些通用的属性和方法,修改
Object
类型是通过
prototype
来完成的:
Object.prototype.getType=function(){
return typeof(this);
}
var array1=new Array();
function func1(a,b){
return a+b;
}
alert(array1.getType());
alert(func1.getType());
上面的代码为所有的对象添加了
getType
方法,作用是返回该对象的类型。两条
alert
语句分别会显示
“object”
和
“function”
。
6.2.4
将函数作为参数传递
在前面已经介绍了函数对象本质,每个函数都被表示为一个特殊的对象,可以方便的将其赋值给一个变量,再通过这个变量名进行函数调用。作为一个变量,它可以以参数的形式传递给另一个函数,这在前面介绍
JavaScript
事件处理机制中已经看到过这样的用法,例如下面的程序将
func1
作为参数传递给
func2
:
function func1(theFunc){
theFunc();
}
function func2(){
alert("ok");
}
func1(func2);
在最后一条语句中,
func2
作为一个对象传递给了
func1
的形参
theFunc
,再由
func1
内部进行
theFunc
的调用。事实上,将函数作为参数传递,或者是将函数赋值给其他变量是所有事件机制的基础。
例如,如果需要在页面载入时进行一些初始化工作,可以先定义一个
init
的初始化函数,再通过
window.onload=init;
语句将其绑定到页面载入完成的事件。这里的
init
就是一个函数对象,它可以加入
window
的
onload
事件列表。
6.2.5
传递给函数的隐含参数:
arguments
当进行函数调用时,除了指定的参数外,还创建一个隐含的对象
——arguments
。
arguments
是一个类似数组但不是数组的对象,说它类似是因为它具有数组一样的访问性质,可以用
arguments[index]
这样的语法取值,拥有数组长度属性
length
。
arguments
对象存储的是实际传递给函数的参数,而不局限于函数声明所定义的参数列表,例如:
function func(a,b){
alert(a);
alert(b);
for(var i=0;i<arguments.length;i++){
alert(arguments[i]);
}
}
func(1,2,3);
代码运行时会依次显示:
1
,
2
,
1
,
2
,
3
。因此,在定义函数的时候,即使不指定参数列表,仍然可以通过
arguments
引用到所获得的参数,这给编程带来了很大的灵活性。
arguments
对象的另一个属性是
callee
,它表示对函数对象本身的引用,这有利于实现无名函数的递归或者保证函数的封装性,例如使用递归来计算
1
到
n
的自然数之和:
var sum=function(n){
if(1==n)return 1;
else return n+sum(n-1);
}
alert(sum(100));
其中函数内部包含了对
sum
自身的调用,然而对于
JavaScript
来说,函数名仅仅是一个变量名,在函数内部调用
sum
即相当于调用一个全局变量,不能很好的体现出是调用自身,所以使用
arguments.callee
属性会是一个较好的办法:
var sum=function(n){
if(1==n)return 1;
else return n+arguments.callee(n-1);
}
alert(sum(100));
callee
属性并不是
arguments
不同于数组对象的惟一特征,下面的代码说明了
arguments
不是由
Array
类型创建:
Array.prototype.p1=1;
alert(new Array().p1);
function func(){
alert(arguments.p1);
}
func();
运行代码可以发现,第一个
alert
语句显示为
1
,即表示数组对象拥有属性
p1
,而
func
调用则显示为
“undefined”
,即
p1
不是
arguments
的属性,由此可见,
arguments
并不是一个数组对象。
6.2.6
函数的
apply
、
call
方法和
length
属性
JavaScript
为函数对象定义了两个方法:
apply
和
call
,它们的作用都是将函数绑定到另外一个对象上去运行,两者仅在定义参数的方式有所区别:
Function.prototype.apply(thisArg,argArray);
Function.prototype.call(thisArg[,arg1[,arg2…]]);
从函数原型可以看到,第一个参数都被取名为
thisArg
,即所有函数内部的
this
指针都会被赋值为
thisArg
,这就实现了将函数作为另外一个对象的方法运行的目的。两个方法除了
thisArg
参数,都是为
Function
对象传递的参数。下面的代码说明了
apply
和
call
方法的工作方式:
//
定义一个函数
func1
,具有属性
p
和方法
A
function func1(){
this.p="func1-";
this.A=function(arg){
alert(this.p+arg);
}
}
//
定义一个函数
func2
,具有属性
p
和方法
B
function func2(){
this.p="func2-";
this.B=function(arg){
alert(this.p+arg);
}
}
var obj1=new func1();
var obj2=new func2();
obj1.A("byA"); //
显示
func1-byA
obj2.B("byB"); //
显示
func2-byB
obj1.A.apply(obj2,["byA"]); //
显示
func2-byA
,其中
[“byA”]
是仅有一个元素的数组,下同
obj2.B.apply(obj1,["byB"]); //
显示
func1-byB
obj1.A.call(obj2,"byA"); //
显示
func2-byA
obj2.B.call(obj1,"byB"); //
显示
func1-byB
可以看出,
obj1
的方法
A
被绑定到
obj2
运行后,整个函数
A
的运行环境就转移到了
obj2
,即
this
指针指向了
obj2
。同样
obj2
的函数
B
也可以绑定到
obj1
对象去运行。代码的最后
4
行显示了
apply
和
call
函数参数形式的区别。
与
arguments
的
length
属性不同,函数对象还有一个属性
length
,它表示函数定义时所指定参数的个数,而非调用时实际传递的参数个数。例如下面的代码将显示
2
:
function sum(a,b){
return a+b;
}
alert(sum.length);
6.2.7
深入认识
JavaScript
中的
this
指针
this
指针是面向对象程序设计中的一项重要概念,它表示当前运行的对象。在实现对象的方法时,可以使用
this
指针来获得该对象自身的引用。
和其他面向对象的语言不同,
JavaScript
中的
this
指针是一个动态的变量,一个方法内的
this
指针并不是始终指向定义该方法的对象的,在上一节讲函数的
apply
和
call
方法时已经有过这样的例子。为了方便理解,再来看下面的例子:
<script language="JavaScript" type="text/javascript">
<!--
//
创建两个空对象
var obj1=new Object();
var obj2=new Object();
//
给两个对象都添加属性
p
,并分别等于
1
和
2
obj1.p=1;
obj2.p=2;
//
给
obj1
添加方法,用于显示
p
的值
obj1.getP=function(){
alert(this.p); //
表面上
this
指针指向的是
obj1
}
//
调用
obj1
的
getP
方法
obj1.getP();
//
使
obj2
的
getP
方法等于
obj1
的
getP
方法
obj2.getP=obj1.getP;
//
调用
obj2
的
getP
方法
obj2.getP();
//-->
</script>
从代码的执行结果看,分别弹出对话框显示
1
和
2
。由此可见,
getP
函数仅定义了一次,在不同的场合运行,显示了不同的运行结果,这是有
this
指针的变化所决定的。在
obj1
的
getP
方法中,
this
就指向了
obj1
对象,而在
obj2
的
getP
方法中,
this
就指向了
obj2
对象,并通过
this
指针引用到了两个对象都具有的属性
p
。
由此可见,
JavaScript
中的
this
指针是一个动态变化的变量,它表明了当前运行该函数的对象。由
this
指针的性质,也可以更好的理解
JavaScript
中对象的本质:一个对象就是由一个或多个属性(方法)组成的集合。每个集合元素不是仅能属于一个集合,而是可以动态的属于多个集合。这样,一个方法(集合元素)由谁调用,
this
指针就指向谁。实际上,前面介绍的
apply
方法和
call
方法都是通过强制改变
this
指针的值来实现的,使
this
指针指向参数所指定的对象,从而达到将一个对象的方法作为另一个对象的方法运行。
每个对象集合的元素(即属性或方法)也是一个独立的部分,全局函数和作为一个对象方法定义的函数之间没有任何区别,因为可以把全局函数和变量看作为
window
对象的方法和属性。也可以使用
new
操作符来操作一个对象的方法来返回一个对象,这样一个对象的方法也就可以定义为类的形式,其中的
this
指针则会指向新创建的对象。在后面可以看到,这时对象名可以起到一个命名空间的作用,这是使用
JavaScript
进行面向对象程序设计的一个技巧。例如:
var namespace1=new Object();
namespace1.class1=function(){
//
初始化对象的代码
}
var obj1=new namespace1.class1();
这里就可以把
namespace1
看成一个命名空间。
由于对象属性(方法)的动态变化特性,一个对象的两个属性(方法)之间的互相引用,必须要通过
this
指针,而其他语言中,
this
关键字是可以省略的。如上面的例子中:
obj1.getP=function(){
alert(this.p); //
表面上
this
指针指向的是
obj1
}
这里的
this
关键字是不可省略的,即不能写成
alert(p)
的形式。这将使得
getP
函数去引用上下文环境中的
p
变量,而不是
obj1
的属性。
6.3
类的实现
6.3.1
理解类的实现机制
在
JavaScript
中可以使用
function
关键字来定义一个
“
类
”
,如何为类添加成员。在函数内通过
this
指针引用的变量或者方法都会成为类的成员,例如:
function class1(){
var s="abc";
this.p1=s;
this.method1=function(){
alert("this is a test method");
}
}
var obj1=new class1();
通过
new
class1()
获得对象
obj1
,对象
obj1
便自动获得了属性
p1
和方法
method1
。
在
JavaScript
中,
function
本身的定义就是类的构造函数,结合前面介绍过的对象的性质以及
new
操作符的用法,下面介绍使用
new
创建对象的过程。
(
1
)当解释器遇到
new
操作符时便创建一个空对象;
(
2
)开始运行
class1
这个函数,并将其中的
this
指针都指向这个新建的对象;
(
3
)因为当给对象不存在的属性赋值时,解释器就会为对象创建该属性,例如在
class1
中,当执行到
this.p1=s
这条语句时,就会添加一个属性
p1
,并把变量
s
的值赋给它,这样函数执行就是初始化这个对象的过程,即实现构造函数的作用;
(
4
)当函数执行完后,
new
操作符就返回初始化后的对象。
通过这整个过程,
JavaScript
中就实现了面向对象的基本机制。由此可见,在
JavaScript
中,
function
的定义实际上就是实现一个对象的构造器,是通过函数来完成的。这种方式的缺点是:
?
将所有的初始化语句、成员定义都放到一起,代码逻辑不够清晰,不易实现复杂的功能。
?
每创建一个类的实例,都要执行一次构造函数。构造函数中定义的属性和方法总被重复的创建,例如:
this.method1=function(){
alert("this is a test method");
}
这里的
method1
每创建一个
class1
的实例,都会被创建一次,造成了内存的浪费。下一节介绍另一种类定义的机制:
prototype
对象,可以解决构造函数中定义类成员带来的缺点。
6.3.2
使用
prototype
对象定义类成员
上一节介绍了类的实现机制以及构造函数的实现,现在介绍另一种为类添加成员的机制:
prototype
对象。当
new
一个
function
时,该对象的成员将自动赋给所创建的对象,例如:
<script language="JavaScript" type="text/javascript">
<!--
//
定义一个只有一个属性
prop
的类
function class1(){
this.prop=1;
}
//
使用函数的
prototype
属性给类定义新成员
class1.prototype.showProp=function(){
alert(this.prop);
}
//
创建
class1
的一个实例
var obj1=new class1();
//
调用通过
prototype
原型对象定义的
showProp
方法
obj1.showProp();
//-->
</script>
prototype
是一个
JavaScript
对象,可以为
prototype
对象添加、修改、删除方法和属性。从而为一个类添加成员定义。
了解了函数的
prototype
对象,现在再来看
new
的执行过程。
(
1
)创建一个新的对象,并让
this
指针指向它;
(
2
)将函数的
prototype
对象的所有成员都赋给这个新对象;
(
3
)执行函数体,对这个对象进行初始化操作;
(
4
)返回(
1
)中创建的对象。
和上一节介绍的
new
的执行过程相比,多了用
prototype
来初始化对象的过程,这也和
prototype
的字面意思相符,它是所对应类的实例的原型。这个初始化过程发生在函数体(构造器)执行之前,所以可以在函数体内部调用
prototype
中定义的属性和方法,例如:
<script language="JavaScript" type="text/javascript">
<!--
//
定义一个只有一个属性
prop
的类
function class1(){
this.prop=1;
this.showProp();
}
//
使用函数的
prototype
属性给类定义新成员
class1.prototype.showProp=function(){
alert(this.prop);
}
//
创建
class1
的一个实例
var obj1=new class1();
//-->
</script>
和上一段代码相比,这里在
class1
的内部调用了
prototype
中定义的方法
showProp
,从而在对象的构造过程中就弹出了对话框,显示
prop
属性的值为
1
。
需要注意,原型对象的定义必须在创建类实例的语句之前,否则它将不会起作用,例如:
<script language="JavaScript" type="text/javascript">
<!--
//
定义一个只有一个属性
prop
的类
function class1(){
this.prop=1;
this.showProp();
}
//
创建
class1
的一个实例
var obj1=new class1();
//
在创建实例的语句之后使用函数的
prototype
属性给类定义新成员,只会对后面创建的对象有效
class1.prototype.showProp=function(){
alert(this.prop);
}
//-->
</script>
这段代码将会产生运行时错误,显示对象没有
showProp
方法,就是因为该方法的定义是在实例化一个类的语句之后。
由此可见,
prototype
对象专用于设计类的成员,它是和一个类紧密相关的,除此之外,
prototype
还有一个重要的属性:
constructor
,表示对该构造函数的引用,例如:
function class1(){
alert(1);
}
class1.prototype.constructor(); //
调用类的构造函数
这段代码运行后将会出现对话框,在上面显示文字
“1”
,从而可以看出一个
prototype
是和一个类的定义紧密相关的。实际上:
class1.prototype.constructor===class1
。
6.3.3
一种
JavaScript
类的设计模式
前面已经介绍了如何定义一个类,如何初始化一个类的实例,且类可以在
function
定义的函数体中添加成员,又可以用
prototype
定义类的成员,编程的代码显得混乱。如何以一种清晰的方式来定义类呢?下面给出了一种类的实现模式。
在
JavaScript
中,由于对象灵活的性质,在构造函数中也可以为类添加成员,在增加灵活性的同时,也增加了代码的复杂度。为了提高代码的可读性和开发效率,可以采用这种定义成员的方式,而使用
prototype
对象来替代,这样
function
的定义就是类的构造函数,符合传统意义类的实现:类名和构造函数名是相同的。例如:
function class1(){
//
构造函数
}
//
成员定义
class1.prototype.someProperty="sample";
class1.prototype.someMethod=function(){
//
方法实现代码
}
虽然上面的代码对于类的定义已经清晰了很多,但每定义一个属性或方法,都需要使用一次
class1.prototype
,不仅代码体积变大,而且易读性还不够。为了进一步改进,可以使用无类型对象的构造方法来指定
prototype
对象,从而实现类的成员定义:
//
定义一个类
class1
function class1(){
//
构造函数
}
//
通过指定
prototype
对象来实现类的成员定义
class1.prototype={
someProperty:"sample",
someMethod:function(){
//
方法代码
},
…//
其他属性和方法
.
}
上面的代码用一种很清晰的方式定义了
class1
,构造函数直接用类名来实现,而成员使用无类型对象来定义,以列表的方式实现了所有属性和方法,并且可以在定义的同时初始化属性的值。这也更象传统意义面向对象语言中类的实现。只是构造函数和类的成员定义被分为了两个部分,这可看成
JavaScript
中定义类的一种固定模式,这样在使用时会更加容易理解。
注意:在一个类的成员之间互相引用,必须通过
this
指针来进行,例如在上面例子中的
someMethod
方法中,如果要使用属性
someProperty
,必须通过
this.someProperty
的形式,因为在
JavaScript
中每个属性和方法都是独立的,它们通过
this
指针联系在一个对象上。
6.4
公有成员、私有成员和静态成员
6.4.1
实现类的公有成员
前面定义的任何类成员都属于公有成员的范畴,该类的任何实例都对外公开这些属性和方法。
6.4.2
实现类的私有成员
私有成员即在类的内部实现中可以共享的成员,不对外公开。
JavaScript
中并没有特殊的机制来定义私有成员,但可以用一些技巧来实现这个功能。
这个技巧主要是通过变量的作用域性质来实现的,在
JavaScript
中,一个函数内部定义的变量称为局部变量,该变量不能够被此函数外的程序所访问,却可以被函数内部定义的嵌套函数所访问。在实现私有成员的过程中,正是利用了这一性质。
前面提到,在类的构造函数中可以为类添加成员,通过这种方式定义的类成员,实际上共享了在构造函数内部定义的局部变量,这些变量就可以看作类的私有成员,例如:
<script language="JavaScript" type="text/javascript">
<!--
function class1(){
var pp=" this is a private
property"; //
私有属性成员
pp
function pm(){ //
私有方法成员
pm
,显示
pp
的值
alert(pp);
}
this.method1=function(){
//
在公有成员中改变私有属性的值
pp="pp has been changed";
}
this.method2=function(){
pm(); //
在公有成员中调用私有方法
}
}
var obj1=new class1();
obj1.method1(); //
调用公有方法
method1
obj1.method2(); //
调用公有方法
method2
//-->
</script>
图
6.4
显示了运行的结果。
这样,就实现了私有属性
pp
和私有方法
pm
。运行完
class1
以后,尽管看上去
pp
和
pm
这些局部变量应该随即消失,但实际上因为
class1
是通过
new
来运行的,它所属的对象还没消失,所以仍然可以通过公开成员来对它们进行操作。
注意:这些局部变量(私有成员),被所有在构造函数中定义的公有方法所共享,而且仅被在构造函数中定义的公有方法所共享。这意味着,在
prototype
中定义的类成员将不能访问在构造体中定义的局部变量(私有成员)。
要使用私有成员,是以牺牲代码可读性为代价的。而且这种实现更多的是一种
JavaScript
技巧,因为它并不是语言本身具有的机制。但这种利用变量作用域性质的技巧,却是值得借鉴的。
6.4.3
实现静态成员
静态成员属于一个类的成员,它可以通过
“
类名
.
静态成员名
”
的方式访问。在
JavaScript
中,可以给一个函数对象直接添加成员来实现静态成员,因为函数也是一个对象,所以对象的相关操作,对函数同样适用。例如:
function class1(){//
构造函数
}
//
静态属性
class1.staticProperty="sample";
//
静态方法
class1.staticMethod=function(){
alert(class1.staticProperty);
}
//
调用静态方法
class1.staticMethod();
通过上面的代码,就为类
class1
添加了一个静态属性和静态方法,并且在静态方法中引用了该类的静态属性。
如果要给每个函数对象都添加通用的静态方法,还可以通过函数对象所对应的类
Function
来实现,例如:
//
给类
Function
添加原型方法:
show ArgsCount
Function.prototype.showArgsCount=function(){
alert(this.length); //
显示函数定义的形参的个数
}
function class1(a){
//
定义一个类
}
//
调用通过
Function
的
prototype
定义的类的静态方法
showArgsCount
class1. showArgsCount ();
由此可见,通过
Function
的
prototype
原型对象,可以给任何函数都加上通用的静态成员,这在实际开发中可以起到很大的作用,比如在著名的
prototype-1.3.1.js
框架中,就给所有的函数定义了以下两个方法:
//
将函数作为一个对象的方法运行
Function.prototype.bind = function(object) {
var __method = this;
return function() {
__method.apply(object, arguments);
}
}
//
将函数作为事件监听器
Function.prototype.bindAsEventListener = function(object) {
var __method = this;
return function(event) {
__method.call(object, event || window.event);
}
}
这两个方法在
prototype-1.3.1
框架中起了很大的作用,具体含义及用法将在后面章节介绍。
6.5
使用
for(…in…)
实现反射机制
6.5.1
什么是反射机制
反射机制指的是程序在运行时能够获取自身的信息。例如一个对象能够在运行时知道自己有哪些方法和属性。
6.5.2
在
JavaScript
中利用
for(…in…)
语句实现反射
在
JavaScript
中有一个很方便的语法来实现反射,即
for(…in…)
语句,其语法如下:
for(var p in obj){
//
语句
}
这里
var p
表示声明的一个变量,用以存储对象
obj
的属性(方法)名称,有了对象名和属性(方法)名,就可以使用方括号语法来调用一个对象的属性(方法):
for(var p in obj){
if(typeof(obj[p]=="function"){
obj[p]();
}else{
alert(obj[p]);
}
}
这段语句遍历
obj
对象的所有属性和方法,遇到属性则弹出它的值,遇到方法则立刻执行。在后面可以看到,在面向对象的
JavaScript
程序设计中,反射机制是很重要的一种技术,它在实现类的继承中发挥了很大的作用。
6.5.3
使用反射来传递样式参数
在
Ajax
编程中,经常要能动态的改变界面元素的样式,这可以通过对象的
style
属性来改变,比如要改变背景色为红色,可以这样写:
element.style.backgroundColor="#ff0000";
其中
style
对象有很多属性,基本上
CSS
里拥有的属性在
JavaScript
中都能够使用。如果一个函数接收参数用用指定一个界面元素的样式,显然一个或几个参数是不能符合要求的,下面是一种实现:
function setStyle(_style){
//
得到要改变样式的界面对象
var element=getElement();
element.style=_style;
}
这样,直接将整个
style
对象作为参数传递了进来,一个
style
对象可能的形式是:
var style={
color:#ffffff,
backgroundColor:#ff0000,
borderWidth:2px
}
这时可以这样调用函数:
setStyle(style);
或者直接写为:
setStyle({ color:#ffffff,backgroundColor:#ff0000,borderWidth:2px});
这段代码看上去没有任何问题,但实际上,在
setStyle
函数内部使用参数
_style
为
element.style
赋值时,如果
element
原先已经有了一定的样式,例如曾经执行过:
element.style.height="20px";
而
_style
中却没有包括对
height
的定义,因此
element
的
height
样式就丢失了,不是最初所要的结果。要解决这个问题,可以用反射机制来重写
setStyle
函数:
function setStyle(_style){
//
得到要改变样式的界面对象
var element=getElement();
for(var p in _style){
element.style[p]=_style[p];
}
}
程序中遍历
_style
的每个属性,得到属性名称,然后再使用方括号语法将
element.style
中的对应的属性赋值为
_style
中的相应属性的值。从而,
element
中仅改变指定的样式,而其他样式不会改变,得到了所要的结果。
6.6
类的继承
6.6.1
利用共享
prototype
实现继承
继承是面向对象开发的又一个重要概念,它可以将现实生活的概念对应到程序逻辑中。例如水果是一个类,具有一些公共的性质;而苹果也是一类,但它们属于水果,所以苹果应该继承于水果。
在
JavaScript
中没有专门的机制来实现类的继承,但可以通过拷贝一个类的
prototype
到另外一个类来实现继承。一种简单的实现如下:
fucntion class1(){
//
构造函数
}
function class2(){
//
构造函数
}
class2.prototype=class1.prototype;
class2.prototype.moreProperty1="xxx";
class2.prototype.moreMethod1=function(){
//
方法实现代码
}
var obj=new class2();
这样,首先是
class2
具有了和
class1
一样的
prototype
,不考虑构造函数,两个类是等价的。随后,又通过
prototype
给
class2
赋予了两个额外的方法。所以
class2
是在
class1
的基础上增加了属性和方法,这就实现了类的继承。
JavaScript
提供了
instanceof
操作符来判断一个对象是否是某个类的实例,对于上面创建的
obj
对象,下面两条语句都是成立的:
obj instanceof class1
obj instanceof class2
表面上看,上面的实现完全可行,
JavaScript
也能够正确的理解这种继承关系,
obj
同时是
class1
和
class2
的实例。事是上不对,
JavaScript
的这种理解实际上是基于一种很简单的策略。看下面的代码,先使用
prototype
让
class2
继承于
class1
,再在
class2
中重复定义
method
方法:
<script language="JavaScript" type="text/javascript">
<!--
//
定义
class1
function class1(){
//
构造函数
}
//
定义
class1
的成员
class1.prototype={
m1:function(){
alert(1);
}
}
//
定义
class2
function class2(){
//
构造函数
}
//
让
class2
继承于
class1
class2.prototype=class1.prototype;
//
给
class2
重复定义方法
method
class2.prototype.method=function(){
alert(2);
}
//
创建两个类的实例
var obj1=new class1();
var obj2=new class2();
//
分别调用两个对象的
method
方法
obj1.method();
obj2.method();
//-->
</script>
从代码执行结果看,弹出了两次对话框
“2”
。由此可见,当对
class2
进行
prototype
的改变时,
class1
的
prototype
也随之改变,即使对
class2
的
prototype
增减一些成员,
class1
的成员也随之改变。所以
class1
和
class2
仅仅是构造函数不同的两个类,它们保持着相同的成员定义。从这里,相信读者已经发现了其中的奥妙:
class1
和
class2
的
prototype
是完全相同的,是对同一个对象的引用。其实从这条赋值语句就可以看出来:
//
让
class2
继承于
class1
class2.prototype=class1.prototype;
在
JavaScript
中,除了基本的数据类型(数字、字符串、布尔等),所有的赋值以及函数参数都是引用传递,而不是值传递。所以上面的语句仅仅是让
class2
的
prototype
对象引用
class1
的
prototype
,造成了类成员定义始终保持一致的效果。从这里也看到了
instanceof
操作符的执行机制,它就是判断一个对象是否是一个
prototype
的实例,因为这里的
obj1
和
obj2
都是对应于同一个
prototype
,所以它们
instanceof
的结果都是相同的。
因此,使用
prototype
引用拷贝实现继承不是一种正确的办法。但在要求不严格的情况下,却也是一种合理的方法,惟一的约束是不允许类成员的覆盖定义。下面一节,将利用反射机制和
prototype
来实现正确的类继承。
6.6.2
利用反射机制和
prototype
实现继承
前面一节介绍的共享
prototype
来实现类的继承,不是一种很好的方法,毕竟两个类是共享的一个
prototype
,任何对成员的重定义都会互相影响,不是严格意义的继承。但在这个思想的基础上,可以利用反射机制来实现类的继承,思路如下:利用
for(…in…)
语句枚举出所有基类
prototype
的成员,并将其赋值给子类的
prototype
对象。例如:
<script language="JavaScript" type="text/javascript">
<!--
function class1(){
//
构造函数
}
class1.prototype={
method:function(){
alert(1);
},
method2:function(){
alert("method2");
}
}
function class2(){
//
构造函数
}
//
让
class2
继承于
class1
for(var p in class1.prototype){
class2.prototype[p]=class1.prototype[p];
}
//
覆盖定义
class1
中的
method
方法
class2.prototype.method=function(){
alert(2);
}
//
创建两个类的实例
var obj1=new class1();
var obj2=new class2();
//
分别调用
obj1
和
obj2
的
method
方法
obj1.method();
obj2.method();
//
分别调用
obj1
和
obj2
的
method2
方法
obj1.method2();
obj2.method2();
//-->
</script>
从运行结果可见,
obj2
中重复定义的
method
已经覆盖了继承的
method
方法,同时
method2
方法未受影响。而且
obj1
中的
method
方法仍然保持了原有的定义。这样,就实现了正确意义的类的继承。为了方便开发,可以为每个类添加一个共有的方法,用以实现类的继承:
//
为类添加静态方法
inherit
表示继承于某类
Function.prototype.inherit=function(baseClass){
for(var p in baseClass.prototype){
this.prototype[p]=baseClass.prototype[p];
}
}
这里使用所有函数对象(类)的共同类
Function
来添加继承方法,这样所有的类都会有一个
inherit
方法,用以实现继承,读者可以仔细理解这种用法。于是,上面代码中的:
//
让
class2
继承于
class1
for(var p in class1.prototype){
class2.prototype[p]=class1.prototype[p];
}
可以改写为:
//
让
class2
继承于
class1
class2.inherit(class1)
这样代码逻辑变的更加清楚,也更容易理解。通过这种方法实现的继承,有一个缺点,就是在
class2
中添加类成员定义时,不能给
prototype
直接赋值,而只能对其属性进行赋值,例如不能写为:
class2.prototype={
//
成员定义
}
而只能写为:
class2.prototype.propertyName=someValue;
class2.prototype.methodName=function(){
//
语句
}
由此可见,这样实现继承仍然要以牺牲一定的代码可读性为代价,在下一节将介绍
prototype-1.3.1
框架(注:
prototype-1.3.1
框架是一个
JavaScript
类库,扩展了基本对象功能,并提供了实用工具详见附录。)中实现的类的继承机制,不仅基类可以用对象直接赋值给
property
,而且在派生类中也可以同样实现,使代码逻辑更加清晰,也更能体现面向对象的语言特点。
6.6.3
prototype-1.3.1
框架中的类继承实现机制
在
prototype-1.3.1
框架中,首先为每个对象都定义了一个
extend
方法:
//
为
Object
类添加静态方法:
extend
Object.extend = function(destination, source) {
for(property in source) {
destination[property] = source[property];
}
return destination;
}
//
通过
Object
类为每个对象添加方法
extend
Object.prototype.extend = function(object) {
return Object.extend.apply(this, [this, object]);
}
Object.extend
方法很容易理解,它是
Object
类的一个静态方法,用于将参数中
source
的所有属性都赋值到
destination
对象中,并返回
destination
的引用。下面解释一下
Object.prototype.extend
的实现,因为
Object
是所有对象的基类,所以这里是为所有的对象都添加一个
extend
方法,函数体中的语句如下:
Object.extend.apply(this,[this,object]);
这一句是将
Object
类的静态方法作为对象的方法运行,第一个参数
this
是指向对象实例自身;第二个参数是一个数组,包括两个元素:对象本身和传进来的对象参数
object
。函数功能是将参数对象
object
的所有属性和方法赋值给调用该方法的对象自身,并返回自身的引用。有了这个方法,下面看类继承的实现:
<script language="JavaScript" type="text/javascript">
<!--
//
定义
extend
方法
Object.extend = function(destination, source) {
for (property in source) {
destination[property] = source[property];
}
return destination;
}
Object.prototype.extend = function(object) {
return Object.extend.apply(this, [this, object]);
}
//
定义
class1
function class1(){
//
构造函数
}
//
定义类
class1
的成员
class1.prototype={
method:function(){
alert("class1");
},
method2:function(){
alert("method2");
}
}
//
定义
class2
function class2(){
//
构造函数
}
//
让
class2
继承于
class1
并定义新成员
class2.prototype=(new class1()).extend({
method:function(){
alert("class2");
}
});
//
创建两个实例
var obj1=new class1();
var obj2=new class2();
//
试验
obj1
和
obj2
的方法
obj1.method();
obj2.method();
obj1.method2();
obj2.method2();
//-->
</script>
从运行结果可以看出,继承被正确的实现了,而且派生类的额外成员也可以以列表的形式加以定义,提高了代码的可读性。下面解释继承的实现:
//
让
class2
继承于
class1
并定义新成员
class2.prototype=(new class1()).extend({
method:function(){
alert("class2");
}
});
上段代码也可以写为:
//
让
class2
继承于
class1
并定义新成员
class2.prototype=class1.prototype.extend({
method:function(){
alert("class2");
}
});
但因为
extend
方法会改变调用该方法对象本身,所以上述调用会改变
class1
的
prototype
的值,犯了和
6.6.1
节中一样的错误。在
prototype-1.3.1
框架中,巧妙的利用
new class1()
来创建一个实例对象,并将实例对象的成员赋值给
class2
的
prototype
。其本质相当于创建了
class1
的
prototype
的一个拷贝,在这个拷贝上进行操作自然不会影响原有类中
prototype
的定义了。
6.7
实现抽象类
6.7.1
抽象类和虚函数
虚函数是类成员中的概念,是只做了一个声明而未实现的方法,具有虚函数的类就称之为抽象类,这些虚函数在派生类中才被实现。抽象类是不能实例化的,因为其中的虚函数并不是一个完整的函数,不能被调用。所以抽象类一般只作为基类被派生以后再使用。
和类的继承一样,
JavaScript
并没有任何机制用于支持抽象类。但利用
JavaScript
语言本身的性质,可以实现自己的抽象类。
6.7.2
在
JavaScript
实现抽象类
在传统面向对象语言中,抽象类中的虚方法必须先被声明,但可以在其他方法中被调用。而在
JavaScript
中,虚方法就可以看作该类中没有定义的方法,但已经通过
this
指针使用了。和传统面向对象不同的是,这里虚方法不需经过声明,而直接使用了。这些方法将在派生类中实现,例如:
<script language="JavaScript" type="text/javascript">
<!--
//
定义
extend
方法
Object.extend = function(destination, source) {
for (property in source) {
destination[property] = source[property];
}
return destination;
}
Object.prototype.extend = function(object) {
return Object.extend.apply(this, [this, object]);
}
//
定义一个抽象基类
base
,无构造函数
function base(){}
base.prototype={
initialize:function(){
this.oninit(); //
调用了一个虚方法
}
}
//
定义
class1
function class1(){
//
构造函数
}
//
让
class1
继承于
base
并实现其中的
oninit
方法
class1.prototype=(new base()).extend({
oninit:function(){ //
实现抽象基类中的
oninit
虚方法
//oninit
函数的实现
}
});
//-->
</script>
这样,当在
class1
的实例中调用继承得到的
initialize
方法时,就会自动执行派生类中的
oninit()
方法。从这里也可以看到解释型语言执行的特点,它们只有在运行到某一个方法调用时,才会检查该方法是否存在,而不会向编译型语言一样在编译阶段就检查方法存在与否。
JavaScript
中则避免了这个问题。当然,如果希望在基类中添加虚方法的一个定义,也是可以的,只要在派生类中覆盖此方法即可。例如:
//
定义一个抽象基类
base
,无构造函数
function base(){}
base.prototype={
initialize:function(){
this.oninit(); //
调用了一个虚方法
},
oninit:function(){} //
虚方法是一个空方法,由派生类实现
}
6.7.3
使用抽象类的示例
仍然以
prototype-1.3.1
为例,其中定义了一个类的创建模型:
//Class
是一个全局对象,有一个方法
create
,用于返回一个类
var Class = {
create: function() {
return function() {
this.initialize.apply(this, arguments);
}
}
}
这里
Class
是一个全局对象,具有一个方法
create
,用于返回一个函数(类),从而声明一个类,可以用如下语法:
var class1=Class.create();
这样和函数的定义方式区分开来,使
JavaScript
语言能够更具备面向对象语言的特点。现在来看这个返回的函数(类):
function(){
this.initialize.apply(this, arguments);
}
这个函数也是一个类的构造函数,当
new
这个类时便会得到执行。它调用了一个
initialize
方法,从名字来看,是类的构造函数。而从类的角度来看,它是一个虚方法,是未定义的。但这个虚方法的实现并不是在派生类中实现的,而是创建完一个类后,在
prototype
中定义的,例如
prototype
可以这样写:
var class1=Class.create();
class1.prototype={
initialize:function(userName){
alert(“hello,”+userName);
}
}
这样,每次创建类的实例时,
initialize
方法都会得到执行,从而实现了将类的构造函数和类成员一起定义的功能。其中,为了能够给构造函数传递参数,使用了这样的语句:
function(){
this.initialize.apply(this, arguments);
}
实际上,这里的
arguments
是
function()
中所传进来的参数,也就是
new class1(args)
中传递进来的
args
,现在要把
args
传递给
initialize
,巧妙的使用了函数的
apply
方法,注意不能写成:
this.initialize(arguments);
这是将
arguments
数组作为一个参数传递给
initialize
方法,而
apply
方法则可以把
arguments
数组对象的元素作为一组参数传递过去,这是一种很巧妙的实现。
尽管这个例子在
prototype-1.3.1
中不是一个抽象类的概念,而是类的一种设计模式。但实际上可以把
Class.create()
返回的类看作所有类的共同基类,它在构造函数中调用了一个虚方法
initialize
,所有继承于它的类都必须实现这个方法,完成构造函数的功能。它们得以实现的本质就是对
prototype
的操作。
6.8
事件设计模式
6.8.1
事件设计概述
事件机制可以使程序逻辑更加符合现实世界,在
JavaScript
中很多对象都有自己的事件,例如按钮就有
onclick
事件,下拉列表框就有
onchange
事件,通过这些事件可以方便编程。那么对于自己定义的类,是否也可以实现事件机制呢?是的,通过事件机制,可以将类设计为独立的模块,通过事件对外通信,提高了程序的开发效率。本节就将详细介绍
JavaScript
中的事件设计模式以及可能遇到的问题。
6.8.2
最简单的事件设计模式
最简单的一种模式是将一个类的方法成员定义为事件,这不需要任何特殊的语法,通常是一个空方法,例如:
function class1(){
//
构造函数
}
class1.prototype={
show:function(){
//show
函数的实现
this.onShow(); //
触发
onShow
事件
},
onShow:function(){} //
定义事件接口
}
上面的代码中,就定义了一个方法:
show()
,同时该方法中调用了
onShow()
方法,这个
onShow()
方法就是对外提供的事件接口,其用法如下:
//
创建
class1
的实例
var obj=new class1();
//
创建
obj
的
onShow
事件处理程序
obj.onShow=function(){
alert("onshow event");
}
//
调用
obj
的
show
方法
obj.show();
代码执行结果如图
6.5
所示。
由此可见,
obj.onShow
方法在类的外部被定义,而在类的内部方法
show()
中被调用,这就实现了事件机制。
上述方法很简单,实际的开发中常用来解决一些简单的事件功能。说它简单,因为它有以下两个缺点:
?
不能够给事件处理程序传递参数,因为是在
show()
这个内部方法中调用事件处理程序的,无法知道外部的参数;
?
每个事件接口仅能够绑定一个事件处理程序,而内部方法则可以使用
attachEvent
或者
addEventListener
方法绑定多个处理程序。
在下面两小节将着重解决这个问题。
6.8.3
给事件处理程序传递参数
给事件处理程序传递参数不仅是自定义事件中存在的问题,也是系统内部对象的事件机制中存在的问题,因为事件机制仅传递一个函数的名称,不带有任何参数的信息,所以无法传递参数进去。例如:
//
定义类
class1
function class1(){
//
构造函数
}
class1.prototype={
show:function(){
//show
函数的实现
this.onShow(); //
触发
onShow
事件
},
onShow:function(){} //
定义事件接口
}
//
创建
class1
的实例
var obj=new class1();
//
创建
obj
的
onShow
事件处理程序
function objOnShow(userName){
alert("hello,"+userName);
}
//
定义变量
userName
var userName="jack";
//
绑定
obj
的
onShow
事件
obj.onShow=objOnShow; //
无法将
userName
这个变量传递进去
//
调用
obj
的
show
方法
obj.show();
注意上面的
obj.onShow=objOnShow
事件绑定语句,不能为了传递
userName
变量进去而写成:
obj.onShow=objOnShow(userName);
或者:
obj.onShow="objOnShow(userName)";
前者是将
objOnShow(userName)
的运行结果赋给了
obj.onShow
,而后者是将字符串
“objOnShow(userName)”
赋给了
obj.onShow
。
要解决这个问题,可以从相反的思路去考虑,不考虑怎么把参数传进去,而是考虑如何构建一个无需参数的事件处理程序,该程序是根据有参数的事件处理程序创建的,是一个外层的封装。现在自定义一个通用的函数来实现这种功能:
//
将有参数的函数封装为无参数的函数
function createFunction(obj,strFunc){
var
args=[]; //
定义
args
用于存储传递给事件处理程序的参数
if(!obj)obj=window; //
如果是全局函数则
obj=window;
//
得到传递给事件处理程序的参数
for(var
i=2;i<arguments.length;i++)args.push(arguments[i]);
//
用无参数函数封装事件处理程序的调用
return function(){
obj[strFunc].apply(obj,args); //
将参数传递给指定的事件处理程序
}
}
该方法将一个有参数的函数封装为一个无参数的函数,不仅对全局函数适用,作为对象方法存在的函数同样适用。该方法首先接收两个参数:
obj
和
strFunc
,
obj
表示事件处理程序所在的对象;
strFunc
表示事件处理程序的名称。除此以外,程序中还利用
arguments
对象处理第二个参数以后的隐式参数,即未定义形参的参数,并在调用事件处理程序时将这些参数传递进去。例如一个事件处理程序是:
someObject.eventHandler=function(_arg1,_arg2){
//
事件处理代码
}
应该调用:
createFunction(someObject,"eventHandler",arg1,arg2);
这就返回一个无参数的函数,在返回的函数中已经包括了传递进去的参数。如果是全局函数作为事件处理程序,事实上它是
window
对象的一个方法,所以可以传递
window
对象作为
obj
参数,为了更清晰一点,也可以指定
obj
为
null
,
createFunction
函数内部会自动认为该函数是全局函数,从而自动把
obj
赋值为
window
。下面来看应用的例子:
<script language="JavaScript" type="text/javascript">
<!--
//
将有参数的函数封装为无参数的函数
function createFunction(obj,strFunc){
var args=[];
if(!obj)obj=window;
for(var
i=2;i<arguments.length;i++)args.push(arguments[i]);
return function(){
obj[strFunc].apply(obj,args);
}
}
//
定义类
class1
function class1(){
//
构造函数
}
class1.prototype={
show:function(){
//show
函数的实现
this.onShow(); //
触发
onShow
事件
},
onShow:function(){} //
定义事件接口
}
//
创建
class1
的实例
var obj=new class1();
//
创建
obj
的
onShow
事件处理程序
function objOnShow(userName){
alert("hello,"+userName);
}
//
定义变量
userName
var userName="jack";
//
绑定
obj
的
onShow
事件
obj.onShow=createFunction(null,"objOnShow",userName);
//
调用
obj
的
show
方法
obj.show();
//-->
</script>
在这段代码中,就将变量
userName
作为参数传递给了
objOnShow
事件处理程序。事实上,
obj.onShow
得到的事件处理程序并不是
objOnShow
,而是由
createFunction
返回的一个无参函数。
通过
createFunction
封装,就可以用一种通用的方案实现参数传递了。这不仅适用于自定义的事件,也适用于系统提供的事件,其原理是完全相同的。
6.8.4
使自定义事件支持多绑定
可以用
attachEvent
或者
addEventListener
方法来实现多个事件处理程序的同时绑定,不会互相冲突,而自定义事件怎样来实现多订阅呢?下面介绍这种实现。要实现多订阅,必定需要一个机制用于存储绑定的多个事件处理程序,在事件发生时同时调用这些事件处理程序。从而达到多订阅的效果,其实现如下:
<script language="JavaScript" type="text/javascript">
<!--
//
定义类
class1
function class1(){
//
构造函数
}
//
定义类成员
class1.prototype={
show:function(){
//show
的代码
//...
//
如果有事件绑定则循环
onshow
数组,触发该事件
if(this.onshow){
for(var i=0;i<this.onshow.length;i++){
this.onshow[i](); //
调用事件处理程序
}
}
},
attachOnShow:function(_eHandler){
if(!this.onshow)this.onshow=[]; //
用数组存储绑定的事件处理程序引用
this.onshow.push(_eHandler);
}
}
var obj=new class1();
//
事件处理程序
1
function onShow1(){
alert(1);
}
//
事件处理程序
2
function onShow2(){
alert(2);
}
//
绑定两个事件处理程序
obj.attachOnShow(onShow1);
obj.attachOnShow(onShow2);
//
调用
show
,触发
onshow
事件
obj.show();
//-->
</script>
从代码的执行结果可以看到,绑定的两个事件处理程序都得到了正确的运行。如果要绑定有参数的事件处理程序,只需加上
createFunction
方法即可,在上一节有过描述。
这种机制基本上说明了处理多事件处理程序的基本思想,但还有改进的余地。例如如果类有多个事件,可以定义一个类似于
attachEvent
的方法,用于统一处理事件绑定。在添加了事件绑定后如果想删除,还可以定义一个
detachEvent
方法用于取消绑定。这些实现的基本思想都是对数组的操作。
6.9
实例:使用面向对象思想处理
cookie
JavaScript
中
Math
对象的功能,它其实就是通过
Math
这个全局对象,把所有的数学计算相关的常量和方法都联系到一起,作为一个整体使用,提高了封装性和使用效率。
cookie
的处理也可以按照这种方法来进行。
6.9.1
需求分析
对于
cookie
的处理,事实上只是封装一些方法,每个对象不会有状态,所以不需要创建一个
cookie
处理类,而只用一个全局对象来联系这些
cookie
操作。对象名可以理解为命名空间。对
cookie
操作经常以下操作。
(
1
)设置
cookie
包括了添加和修改功能,事实上如果原有
cookie
名称已经存在,那么添加此
cookie
就相当于修改了此
cookie
。在设置
cookie
的时候可能还会有一些可选项,用于指定
cookie
的声明周期、访问路径以及访问域。为了让
cookie
中能够存储中文,该方法中还需要对存储的值进行编码。
(
2
)删除一个
cookie
,删除
cookie
只需将一个
cookie
的过期事件设置为过去的一个时间即可,它接收一个
cookie
的名称为参数,从而删除此
cookie
。
(
3
)取一个
cookie
的值,该方法接收
cookie
名称为参数,返回该
cookie
的值。因为在存储该值的时候已经进行了编码,所以取值时应该能自动解码,然后返回。
针对这些需求,下一小节将实现这些功能。
6.9.2
创建
Cookie
对象
因为是作为类名或者命名空间的作用,所以和
Math
对象类似,这里使用
Cookie
来表示该对象:
var Cookie=new Object();
6.9.3
实现设置
Cookie
的方法
方法为:
setCookie(name,value,option);
其中
name
是要设置
cookie
的名称;
value
是设置
cookie
的值;
option
包括了其他选项,是一个对象作为参数。其实现如下:
Cookie.setCookie=function(name,value,option){
//
用于存储赋值给
document.cookie
的
cookie
格式字符串
var str=name+"="+escape(value);
if(option){
//
如果设置了过期时间
if(option.expireDays){
var date=new Date();
var ms=option.expireDays*24*3600*1000;
date.setTime(date.getTime()+ms);
str+="; expires="+date.toGMTString();
}
if(option.path)str+="; path="+path; //
设置访问路径
if(option.domain)str+="; domain"+domain; //
设置访问主机
if(option.secure)str+="; true"; //
设置安全性
}
document.cookie=str;
}
6.9.4
实现取
Cookie
值的方法
方法为:
getCookie(name);
其中
name
是指定
cookie
的名称,从而根据名称返回相应的值。实现如下:
Cookie.getCookie=function(name){
var cookieArray=document.cookie.split(";
"); //
得到分割的
cookie
名值对
var cookie=new Object();
for(var i=0;i<cookieArray.length;i++){
var
arr=cookieArray[i].split("="); //
将名和值分开
if(arr[0]==name)return unescape(arr[1]); //
如果是指定的
cookie
,则返回它的值
}
return "";
}
6.9.5
实现删除
Cookie
的方法
方法为:
deleteCookie(name);
其中
name
是指定
cookie
的名称,从而根据这个名称删除相应的
cookie
。在实现中,删除
cookie
是通过调用
setCookie
来完成的,将
option
的
expireDays
属性指定为负数即可:
Cookie.deleteCookie=function(name){
this.setCookie(name,"",{expireDays:-1}); //
将过期时间设置为过去来删除一个
cookie
}
通过下面的代码,整个
Cookie
对象创建完毕后,可以将其放到一个大括号中来定义,例如:
var Cookie={
setCookie:function(){},
getCookie:function(){},
deleteCookie:function(){}
}
通过这种形式,可以让
Cookie
的功能更加清晰,它作为一个全局对象,大大方便了对
Cookie
的操作,例如:
Cookie.setCookie("user","jack");
alert(Cookie.getCookie("user"));
Cookie.deleteCookie("user");
alert(Cookie.getCookie("user"));
上面的代码就先建立了一个名为
user
的
cookie
,然后删除了该
cookie
。两次
alert
输出语句显示了执行的效果。
本节通过建立一个
Cookie
对象来处理
cookie
,方便了操作,也体现了面向对象的编程思想:把相关的功能封装在一个对象中。考虑到
JavaScript
语言的特点,本章没有选择需要创建类的面向对象编程的例子,那和一般面向对象语言没有大的不同。而是以
JavaScript
中可以直接创建对象为特点介绍了
Cookie
对象的实现及其工作原理。事实上这也和
JavaScript
内部对象
Math
的工作原理是类似的。