-
6.1 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))); //“function” alert(typeof(new Function()));//“function” alert(typeof(Array)); //“function” alert(typeof(Object)); //“function”
alert(typeof(new Array()));//object alert(typeof(new Date()));//object alert(typeof(new Object()));//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的工作原理是类似的。
|