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 οnchange="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.οnlοad=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 的定义实际上就是实现一个对象的构造器,是通过函数来完成的。这种方式的缺点是:
1. 将所有的初始化语句、成员定义都放到一起,代码逻辑不够清晰,不易实现复杂的功能。
2. 每创建一个类的实例,都要执行一次构造函数。构造函数中定义的属性和方法总被重复的创建,例如:
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.οnshοw=[]; // 用数组存储绑定的事件处理程序引用
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 的工作原理是类似的。