《面向对象的 JavaScript 编程及其 Scope 处理.pdf》由会员分享,可在线阅读,更多相关《面向对象的 JavaScript 编程及其 Scope 处理.pdf(12页珍藏版)》请在淘文阁 - 分享文档赚钱的网站上搜索。
1、面向对象的 面向对象的 JavaScript 编程及其 编程及其 Scope 处理处理秦 剑,软件工程师,IBM邵 煜,工程师,IBM陈 序明,架构师,IBM简介:简介:在面向对象的 JavaScript 编程中,我们常常会将一些数据结构和操作封装成对象以达到继承和重用的目的。然而层层封装和继承再加上 JavaScript 中特殊关键字 this 的使用,使得 JavaScript 当前运行 Context 看起来非常的混乱。很多 developer 为了获取运行时正确的 Context,常常不得已将function 或者 Object 声明在全局 Global Context 中。零散的声明
2、、复杂的引用再加上客户端代码的不断膨胀,最终使得客户端的 JavaScript 代码变成一场灾难。发布日期:发布日期:2011 年 3 月 28 日 级别:级别:高级访问情况访问情况 733 次浏览建议:建议:平均分(共 8 个评分)本文首先对 JavaScript 的机制进行讲解,并结合当前流行的开源 JavaScript 框架讲解如何在 JavaScript 中实现面向对象和继承机制;之后本文将对面向对象 JavaScript 编程中容易引起误解和 Scope 的几个问题做详细阐述;最后针对面向对象的 JavaScript 编程中的 Scope 问题给出几点建议。面向对象和模拟继承Java
3、Script 是一种弱类型解释运行的脚本语言,就语言本身来讲它不是一门面向对象语言。但是我们可以利用一些语言的特性来模拟面向对象编程和继承机制,这所有的一切都需要从 JavaScript 中的 function 说起。Function 是一个定义一次但可以调用或执行无数次的 JavaScript 代码片段。Function 可以有零个或者多个输入参数和一个返回值,它通常被用来完成一些计算或者事务处理等任务。通常我们这样定义一个function:清单清单 1.Function 定义示例定义示例在 JavaScript 中,function 不仅是是一种语法结构,它还可以被作为数据。这意味着它可以
4、被赋值给变量,在对象或数组中的元素的属性存储,或者作为函数参数传递等等。例如:function distance(x1,y1,x2,y2)var dx=x2-x1;var dy=y2-y1;return Math.sqrt(dx*dx+dy*dy);清单清单 2.Function 作为数据示例作为数据示例当在一个object 中定义和调用一个 function 时,这个 function 被称作该 object 的一个方法。需要注意的是当这个 function 被调用时,这个 object 会以隐含参数的形式传入到 function 里,function 内部可以通过this 关键字来引用这个
5、 object 的属性。例如下面这个例子运行结果calculator.result的值为 2。清单清单 3.This 关键字示例关键字示例在 JavaScript 中,对象的创建通常是通过new运算符来完成的。new 关键字后面必须是一个可执行的 function。例如:清单清单 4.New 关键字示例关键字示例当上面这条创建语句被执行时,首先会创建一个空的对象赋给前面的变量,然后再调用后面紧跟的function,并将这个空的对象作为隐含参数传入到 function 内部。这样在 function 内部就可以通过this关键字引用该对象,做一些对象初始化工作。这样的一个 function 通常
6、被称为构造方法或者简单构造方法。例如:清单清单 5.构造方法示例构造方法示例讲到这里我们已经有了对象、函数、作为数据的函数、对象方法和构造函数,于是我们可以使用这些JavaScript 的特性模拟写出类似于 java 风格面向对象的代码。例如:清单清单 6.模拟面向对象示例模拟面向对象示例var d=distance;d(1,1,2,2);var calculator=operand1:1,operand2:1,compute:function()this.result=this.operand1+this.operand2;pute();alert(calculator.result);va
7、r array=new Array(10);var today=new Date();function Rectangle(w,h)this.width=w;this.height=h;var rect1=new Rectangle(2,4);function Rectangle(w,h)this.width=w;this.height=h;this.area=function()上面的代码似乎十分完美,但是看起来似乎还是缺少一些东西。面向对象的精髓是对事物的抽象和继承,以到达代码重用的目的。然而在 JavaScript 中如何做到这一点呢?在 JavaScript 中每一个 object 都
8、有一个 prototype 属性,这个属性引用了另一个 object 对象。当你需要读取一个 object 中的某个属性 p 时,JavaScript 引擎首先会查找这个 object 中是否存在属性 p。如果该属性存在则返回该属性值,如果该属性不存在则会在这个 object 的 prototype 对象中查找是否存在属性 p。这样做会有两点好处:第一,在每个类中可以抽象出来一些共有的属性和方法定义在 prototype 对象中。当使用构造函数创建多个类的实例时,多个实例使用同一 prototype 对象,这样大大减小了对内存的占用量。第二,新创建的对象的属性是在对象创建时,由 prototy
9、pe 对象带来的,这样就可以实现属性和方法的继承了。在前面对 function 的介绍中,我们曾提到当new操作符被调用时,它会创建一个空的对象赋给变量并且调用后面的构造函数。实际上在 new操作符创建一个空对象后,它还会为这个对象设置它的 prototype属性,这个属性值等于它的构造函数的prototype 属性值。所有的函数在其定义时就已经自动创建和初始化好了 prototype 属性。这个初始化好的 prototype 属性指向一个只包含一个 constructor属性的对象,并且这个constructor 属性指向这个 function 自己。这就解释了为什么每一个 object 都
10、会有一个 constructor 属性。如果你运行下面的代码,打印出一个名为 Object 的 function。清单清单 7.Constructor 属性示例属性示例讲到这里我们对如何在 JavaScript 中模拟面向对象编程有了清楚的了解。我们可以通过将一些共有的属性和方法定义在构造方法来实现属性的继承,例如,清单清单 8.模拟继承示例模拟继承示例另外如果需要继承另一个类并且重写和添加一些方法的时候,你可能就需要拷贝这个类的构造方法的prototype 对象中的属性和方法,然后再根据需要重写和添加一些方法和属性了。JavaScript 框架中的面向对象的实现 return this.wi
11、dth*this.height;var rect1=new Rectangle(2,4);rect1.area();var obj=new Object();alert(obj.constructor);function Rectangle(w,h)this.width=w;this.height=h;Rectangle.prototype.area=function()return this.width*this.height;在前面一节中我们介绍了一些 JavaScript 基本的特性和如何利用这些特性模拟实现面向对象和继承。目前开源社区有众多流行的 JavaScript 类库,如 jQu
12、ery、Extjs、Dojo 等,他们各自也都对面向对象编程和继承等机制有着不同的支持。那么这些开源 JavaScript 是如何实现面向对象和继承机制的呢?接下来我们介绍一下这些开源框架是如何实现这些机制的。清单清单 9.在在 Dojo 中声明一个类中声明一个类通过上面的声明,可以得到一个名为“com.test.MyClass”的 Dojo 类,它继承自“com.ibm.MySuperClassOne”和“com.ibm.MySuperClassTwo”两个类。类型声明后,就可用如下代码获得一个类的实例。其与前文所述的创建对象的方式是一致的。清单清单 10.获得 获得 Dojo 类的实例类的
13、实例在 ExtJS 中,类型间的继承关系式通过 extend 这个函数来实现的,下面给出了 ExtJS 中声明类之间继承关系的方式。清单清单 11.ExtJS 中的中的 extend 函数的使用函数的使用或者对于第二中继承声明方式,除了 SuperClass 事先已定义好之外,MyClass 也需要事先定义,而第一种则是由 extend 函数直接构造出类型,即是在声明类的同时建立了继承关系。由此可见,这两种 extend 的调用方式的使用场景是不同的。那么 Dojo、ExtJS 等 JavaScript 库中的这种面向对象的继承是如何实现的呢?这就需要用到前文中提到的 JavaScript 语
14、法中比较特殊的 prototype 的机制。由前文可知,prototye 是一个函数的内置属性,这个属性指向另一个对象的引用,在这个对象中定义的属性和方法将被这个函数经过 new 操作得到的对象共享。如果某个函数被视作 JavaScript 中的类型定义,那么这个函数的 prototype 对象的属性和方法将会被这个函数所代表的类的实例共享,也就是说 prototype 对象是这个类的的代表和载体。因此,利用 prototype 的特性,Dojo、ExtJS 等 JavaScript 类库模拟了面向对象的语言机制。而 Dojo 和 ExtJS 中的面向对象继承功能有一个很大的不同,就是 Ext
15、JS 实现的是单继承,即一个子类只有一个父类,而 Dojo 实现了多继承,即一个子类可以有多个父类,这从上面给出的代码示例中可以看出。在实现上,ExtJS 中每声明一个类,都会将这个类的方法、属性放到这个类的 prototype 中去,然后将这个类的 ptototype 对象的类型的 prototype 指向父类的 prototype 对象,如下图所示。dojo.declare(com.test.MyClass,com.test.MySuperClassOne,com.test.MySuperClassTwo,myName:Shao Yu,myMethod:function()return I
16、 am Shao Yu;);var myObj =new com.test.myClass();MyClass=Ext.extend(SuperClass,/*新加入的属性方法*/);Ext.extend(MyClass,SuperClass,/*新加入的属性方法*/);图图 1.ExtJS 中类的 中类的 prototype 的结构关系的结构关系在上图中,可以清楚地看到 MyClass 的 prototype 的对象是一个临时生成的函数(类)实例化生成的,而这个临时的类的 prototype 指向了 SuperClass 的 prototpye 对象,于是根据 JavaScript 中 pr
17、ototype 的传递特性,MyClass 的实例可以调用 MyClass 的 prototype 对象的方法,进而可以调用 MyClass 的 prototype 对象的类即图中的临时类的 prototype 对象中的方法,也是 SuperClass 的 prototype 对象的方法。这样便实现了 MyClass 与 SuperClass 之间的继承关系。上面解释了 MyClass 和 SuperClass 的继承关系的,这是单继承的实现方式,若是有多个类之间的单继承,则可以通过上面的方式串成一个继承链,如下图所示。图图 2.ExtJS 中的继承链中的继承链ExtJS 只支持单继承,即一个
18、子类最多有一个父类。而由于 Dojo 支持多继承,所以一个子类是允许有多个父类的。Dojo 首先会将多继承的关系通过某种算法转换为单继承链,在利用上面描述的 prototype链的方式将单继承链实现。如下图所示。图图 3.多继承关系转换为单继承链多继承关系转换为单继承链Dojo 中使用的多继承转换算法叫做 C3 Method Resolution Order 是一种比较通用的算法。除了实现继承关系,ExtJS、Dojo 等 JavaScript 库还为面向对象实现了一些其他的功能和机制,比如构造函数、父类函数调用等等,这在后续的相关文章中将会详细论述。面向对象 JavaScript 中 Sco
19、pe 问题的处理在面向对象的 JavaScript 编程中,我们包装继承了大量的对象,同时对象之间还有很多复杂的引用关系,这就使得很多时候我们在 function 调用时的 Scope 不是我们想要的 Scope。产生这些问题的原因最终都源于我们没有深入的了解 JavaScript 的机制。在下面的一节我们将就这些容易产生 Scope 问题的机制做一个简单的介绍,并给出一些如何避免 Scope 问题的建议。Scope 和 Closure很多程序员在使用 JavaScript 的时候都会对 function 的 Scope 运行范围和 Closure 闭包的使用很困惑,这都源于他们对 JavaS
20、cript 原理的不了解。在 JavaScript 中 function 是作用于词法范围而不是动态运行范围。也就是说 function 的作用范围是它声明的范围,而不是它在执行时的范围。简单的来说,一个 function 执行时的上下文环境 Context 是在其定义的时候就固定下来了,就是它定义的时的作用范围。这点是我们需要注意的,很多时候我们动态的将某个方法注入到一个对象内部,然而在运行时总是得不到想要的上下文环境,这就是因为没有正确理解 JavaScript 的 Scope。当一个 function 被 JavaScript 引擎调用执行时,这个 function 的 Scope 才起
21、作用被加到 Scope 链中。然后将一个叫做 Call Object 调用对象或者运行对象加到 Scope 的最前面。这个调用对象初始化时会加入一个 arguments 属性,用来引用 function 调用时的参数。如果这个 function 显示的定义了调用参数的话,这些参数也会被加入到这个对象中。之后再这个函数运行中所有的局部变量也都将包含在这个对象中。这也就是在 function 体内部既可以通过 arguments 数组,又可以直接通过显示定义参数名来引用调用时传入参数的原因。需要注意的是,这个调用对象和通过this关键字引用的对象是两个概念。调用对象是 function 在运行时的
22、 Scope,其中包含了 function 在运行时的全部参数和局部变量。通过this 关键字引用的对象,是当 function 作为一个这个对象的方法运行时对这个对象的引用。如果这个 function 没有被定义在一个对象中时,传入给this 的对象是全局对象,因此在这个 function 内部通过this取到的变量就是全局定义的变量。例如下面代码运行结果应该弹出Hello,I am Qin Jian:清单清单 12.闭包定义示例闭包定义示例在 JavaScript 中允许定义匿名 function 和在 function 中嵌套定义另一个 function,由于 Scope 链的作用,fu
23、nction 的内部总是可以访问到外部的变量而外部不能访问到内部的变量。另外由于 Call Object 的机制,使得我们可以使用 function 嵌套定义和调用来做很多事情。在 JavaScript 中这样的调用被称为var hello=Hello,I am Shao Yu!;function sayHello()var hello=Hello,I am Qin Jian.function anotherFun()alert(this.hello);anotherFun();sayHello();Closure 闭包。清单清单 13.用闭包生成唯一 用闭包生成唯一 ID上面这段代码很清楚的
24、说明了闭包做了件什么事情。当外层的function被执行时,它的Scope被加入到Scope chain上,然后为它创建call object 加入到 scope 中。之后又创建了局部变量 id 将它保存在该 function 的 call object 中。如果没有“return function()return id+;”这条语句的话,外层的 function 将要运行结束退出,同时他的 call object 会被释放,scope 会从 Scope chain 上移除。由于“return function()return id+;”这条语句创建了一个内部的 function,并将其引用返
25、回给一个变量。这样内部 function 的 scope 会被添加到之前外部 function 的 scope 之下,使得在外部 function 运行结束后他的 scope 不能被撤销和释放。这样就是用外部 function 的 call object 保存了变量 id,并且除了内部的 function 以为没有别的程序能访问到这个变量。虽然看起来有些复杂和拗口,但是闭包确实是以项非常有用的功能,经常用来保存变量控制访问域等用途。例如下面这个例子:清单清单 14.利用 利用 call object 和闭包保存数据和闭包保存数据Call 和 Apply 方法在 JavaScript 中提供了两
26、个非常有意思的方法 call 和 apply,它使得你可以将一个 function 作为另一个object 的对象方法来调用。也就是说你可以选择 function 调用时,传入给this关键字的对象。这两个方法第一个参数是相同的,为你想要传入给this 关键字的对象。不同之处是,call 方法直接将function 参数列在后面,而 apply 方法是将所有 function 参数以一个数组的形式传入。例如下面这个例子:清单清单 15.call apply 方法示例方法示例这两个方法在面向对象的 JavaScript 编程中非常有用,有时你希望给某个对象添加一个事件监听,然而回调方法的 con
27、text 却不一定是你需要的。这时你就需要 call 或者 apply 方法了。uniqueID=(function()var id=0;return function()return id+;)();alert(uniqueID();/显示结果 0 alert(uniqueID();/显示结果 1 alert(uniqueID();/显示结果 2 function makefunc(x)return function()return x;var a=makefunc(I am Qin Jian),makefunc(I am shao yu),makefunc(I am xu ming);al
28、ert(a0();/显示结果 I am Qin Jian alert(a1();/显示结果 I am shao yu alert(a2();/显示结果 I am xu ming Var fun=function(arg1,arg2)/do anything you want here fun.call(object,arg1,arg2);fun.apply(object,arg1,arg2);Eval 方法eval 方法用于执行某个字符串中的 JavaScript 脚本,并反回执行结果。它能够允许动态生成的变量、表达式、函数调用、语句段得到执行,使得 JavaScript 的程序的设计更为灵活
29、,比如通过 Ajax 方式从服务器端获得代表 JavaScript 脚本的字符串,然后就可以用这发方法在浏览器端来执行这段脚本。因为传统的 Ajax 通讯设计更多的是在服务器端与浏览器端交换数据,但在这个方法的支持下就可以在两者之间交换逻辑(在 JDK1.6 支持在 JVM 中运行 JavaScript 脚本),所以这是一种很有趣很神奇的事情。eval 用法很灵活也比较简单,调用时将要执行的 JavaScript 脚本字符串作为参数传给这个方法。比较常用的就是将服务器端发送过来的 json 字符串在浏览器端转化为 json 对象,如下面这个例子。清单清单 16.eval 方法将方法将 json
30、 字符串转换为对象字符串转换为对象在上面的代码中,在 json 字符串又加上了一对括号,这样做可以迫使 eval 函数在评估 JavaScript 代码时强制将原最外层括号内的内容作为表达式来执行从而得到 json 对象,而非作为语句来执行。所以只有用新的小括号将原来的 json 字符串包起来才能够转换出所需的 json 对象。对于执行一般的包含在字符串中的 JavaScript 语句,自然就不需要像上面那样再次添加括号,如下例所示。清单清单 17.eval 方法执行包含在字符串中的方法执行包含在字符串中的 json 语句语句上面的函数会在弹出的提示对话框中输出变量 m 的值。eval 函数的
31、里面的语句只是一个简单的赋值,但有一个问题是值得注意的,就是 eval 中的语句是在什么样的 scope 中执行,为了更好的说明问题,执行下例中所示的代码。清单清单 18.测试测试 eval 中的中的 JavaScript 语句执行的 语句执行的 scope可以得到,变量 m 所在的 scope 是在 testStatement 函数内,从 eval 调用的位置来看这个执行结果是合理的。接下来用 window.eval 替换清单 3 中的 eval 后,在 Firefox 上执行(注意是 Firefox),从执行后的结果中可以发现,这时的 m 的作用域变成了 window,也就是说 m 变成了
32、一个全局变量。那么在 IE 下会如何呢,经测试发现,使用 window.eval 和使用 eval 在 IE 浏览器上是相同的结果,即 m 的作用域在 testStatement 函数中。但是 IE 浏览器提供了另外的一个方法 execScript,它会将输入的 var jsonString=name:qinjian:I am qinjian,shaoyu:I am shaoyu;var jsonObject=eval(+jsonString+);alert(jsonObject.name.qinjian)function testStatement()eval(var m=1);alert(
33、m);testStatement();function testStatement()eval(var m=1);alert(m);testStatement();alert(typeof m);JavaScript 脚本字符串放到全局作用域下执行。所以总结一下就是:eval 方法是将输入的代码在局部作用域下执行,若要实现 JavaScript 字符串中代码的全局作用域下执行的效果,或者把 eval 放到全作用域下调用,或者在 Firefox 下使用 window.eval,在 IE 下使用 execScript。最后需要提醒的是,因为 eval 的执行效率较低,所以一般在程序中最好不要频繁使
34、用。避免 Scope 问题的几点建议巧用闭包。了解了闭包利用 Call Object 的产生原理后,我们就很容易利用闭包,如 namespace 的隔离和保存局部数据等等。同时闭包也很容易使我们的 this 实例不是我们想要的 this 示例。这时我们就可以利用内层可以访问外层变量的特点将外层 this 实例赋给一个变量,内层可以通过这个变量顺利访问外层 this 实例。例如下面代码片段:清单清单 19.处理闭包中 处理闭包中 scope 问题示例问题示例使用 Call 和 Apply 指定 function 调用时的 Scope。我们会经常会提供一种类似回调函数的机制,在设计这个接口的时候并
35、不提供函数的实现只负责调用该接口。例如对外提供一个 onclick 事件接口。由于 JavaScript 的 Scope 机制是词法范围而不是动态运行范围,因此回调函数运行的 Scope 往往不是我们想要的。对于这种情况我们可以在注册回调函数的时候,将 Scope 一起传进来,在需要调用该回调函数时使用 call 和 apply 方法来调用该方法,同时将需要的 scope 传递给这个函数。例如 dojo 的事件机制就是这样处理这个问题的,在注册回调函数的同时你可以指定函数调用的 Scope。清单清单 20.Dojo 中传递中传递 Scope 示例示例慎用 Eval。Eval 可以动态的从字符串
36、中执行代码,它使得 JavaScript 的功能更加强大。通常 Eval 有全局或者局部两种运行 Scope,之前我们说过用户可以指定是否在全局运行。当其运行在一个局部的 function 中时,需要注意的是这时 Eval 运行在这个 function 的 call object 中它可以通过 this 关键字访问到这个 function 的 scope。另外,Eval 方法运行效率非常低,且运行的脚本是未经验证的,因此在使用 Eval 方法时要十分慎重。.switchAds:function(index)var _this=this;dojo.fadeOut(node:_this.adBac
37、k,duration:500,onEnd:function()dojo.fadeIn(node:_this.adBack,duration:500,onEnd:function()_this.currentAd=index;).play();).play();,.dojo.connect(node,onclick,this,this._collapse);小结本文首先介绍了 JavaScript 语言的一些特性并如何利用这些特性实现面向对象和继承机制;随后我们结合目前流行的开源 JavaScript 类库,讲解他们是如何实现面向对象和继承机制等特性的;之后我们对JavaScript 面向对象编
38、程中几个容易引起误解和 Scope 问题的要点,包括闭包、call 函数、apply 函数,eval 函数等做了详细的讲解;最后根据作者的开发经验给出几个如何处理 Scope 问题的建议供大家参考,希望对大家以后的开发工作有一定的帮助。参考资料学习学习参考 维基百科:JavaScript。参考 Dojo 参考文档:DojoToolKit。developerWorks Web development 专区:通过专门关于 Web 技术的文章和教程,扩展您在网站开发方面的技能。developerWorks Ajax 资源中心:这是有关 Ajax 编程模型信息的一站式中心,包括很多文档、教程、论坛、b
39、log、wiki 和新闻。任何 Ajax 的新信息都能在这里找到。developerWorks Web 2.0 资源中心,这是有关 Web 2.0 相关信息的一站式中心,包括大量 Web 2.0 技术文章、教程、下载和相关技术资源。您还可以通过 Web 2.0 新手入门 栏目,迅速了解 Web 2.0 的相关概念。查看 HTML5 专题,了解更多和 HTML5 相关的知识和动向。讨论讨论加入 developerWorks 中文社区。作者简介秦剑,IBM 中国软件实验室(CSDL BJ)成员,是BTT(Bank Transformation Toolkit)组软件工程师,专注于Web2.0下一代银行前端开发。对HTML、CSS、Dojo、Ajax有浓厚的兴趣。邵煜,IBM 中国软件实验室(CSDL BJ)成员,是 BTT(Bank Transformation Toolkit)组的银行渠道解决方案资深软件工程师,是 BTT 产品中 Web2.0 前端架构师。从事过 BTT 产品的售前、架构、开发、测试等工作,对开源框架、Web2.0、Eclipse 等技术以及网上银行、柜员系统、手机银行等业务有着丰富的经验和理解。你可以通过 联系他。陈序明,IBM 中国软件实验室(CSDL BJ)成员,WM-BTT Solution 架构师,对产品研发、软件架构、以及银行业很感兴趣。
限制150内