JavaScript 使用面向对象的技术创建高级 Web 应用程序.docx
《JavaScript 使用面向对象的技术创建高级 Web 应用程序.docx》由会员分享,可在线阅读,更多相关《JavaScript 使用面向对象的技术创建高级 Web 应用程序.docx(22页珍藏版)》请在冰豆网上搜索。
JavaScript使用面向对象的技术创建高级Web应用程序
JavaScript
使用面向对象的技术创建高级Web应用程序
RayDjajadinata
本文讨论:
∙JavaScript是基于原型的语言
∙用JavaScript进行面向对象的编程
∙JavaScript编码技巧
∙JavaScript的未来
本文使用了以下技术:
JavaScript
目录
JavaScript对象是词典
JavaScript函数是最棒的
构造函数而不是类
原型
静态属性和方法
闭包
模拟私有属性
从类继承
模拟命名空间
应当这样编写JavaScript代码吗?
展望
最近,我面试了一个有五年Web应用程序开发经验的软件开发人员。
四年半来她一直在从事JavaScript相关的工作,她自认为JavaScript技能非常好,但在不久之后我就发现实际上她对JavaScript知之甚少。
话虽这样说,但我确实没有责备她的意思。
JavaScript真的是很有趣。
很多人(包括我自己,直到最近!
)都认为自己很擅长JavaScript语言,因为他们都知道C/C++/C#,或者有一些以前的编程经验。
在某种程度上,这种假设并不是完全没有根据的。
用JavaScript很容易做些简单的事情。
入门的门槛很低,该语言很宽松,它不需要您知道很多细节就可以开始用它进行编码。
甚至非编程人员也可能用它在几个小时内为主页编写一些有用的脚本。
的确,直到最近,仅仅凭借MSDN®DHTML参考资料和我的C++/C#经验,我也总能勉强利用这点JavaScript知识完成一些任务。
只是当我开始编写真实的AJAX应用程序时,我才意识到实际上我的JavaScript知识还非常不够。
这个新一代的Web应用程序的复杂性和交互性需要程序员以完全不同的方法来编写JavaScript代码。
它们是真正的JavaScript应用程序!
我们在编写一次性脚本时一直采用的方法已完全不再有效。
面向对象编程(OOP)是一种流行的编程方法,很多JavaScript库中都使用这种方法,以便更好地管理和维护基本代码。
JavaScript支持OOP,但与诸如C++、C#或VisualBasic®等流行的Microsoft®.NETFramework兼容语言相比,它支持OOP的方式非常不同,因此主要使用这些语言的开发人员开始可能会觉得在JavaScript中使用OOP很奇怪而且不直观。
我写本文就是为了深入讨论JavaScript语言实际上如何支持面向对象编程,以及您如何使用这一支持在JavaScript中高效地进行面向对象开发。
下面首先讨论对象(还能先讨论其他别的什么呢?
)。
JavaScript对象是词典
在C++或C#中,在谈论对象时,是指类或结构的实例。
对象有不同的属性和方法,具体取决于将它们实例化的模板(即类)。
而JavaScript对象却不是这样。
在JavaScript中,对象只是一组名称/值对,就是说,将JavaScript对象视为包含字符串关键字的词典。
我们可以使用熟悉的“.”(点)运算符或“[]”运算符,来获得和设置对象的属性,这是在处理词典时通常采用的方法。
以下代码段
varuserObject=newObject();
userObject.lastLoginTime=newDate();
alert(userObject.lastLoginTime);
的功能与下面的代码段完全相同:
varuserObject={};//equivalenttonewObject()
userObject[“lastLoginTime”]=newDate();
alert(userObject[“lastLoginTime”]);
我们还可以直接在userObject的定义中定义lastLoginTime属性,如下所示:
varuserObject={“lastLoginTime”:
newDate()};
alert(userObject.lastLoginTime);
注意,它与C#3.0对象初始值非常相似。
而且,熟悉Python的人会发现在第二和第三个代码段中实例化userObject的方法与在Python中指定词典的方法完全相同。
唯一的差异是JavaScript对象/词典只接受字符串关键字,而不是像Python词典那样接受可哈希化的对象。
这些示例还显示JavaScript对象比C++或C#对象具有更大的可延展性。
您不必预先声明属性lastLoginTime—如果userObject没有该名称的属性,该属性将被直接添加到userObject。
如果记住JavaScript对象是词典,您就不会对此感到吃惊了,毕竟,我们一直在向词典添加新关键字(和其各自的值)。
这样,我们就有了对象属性。
对象方法呢?
同样,JavaScript与C++/C#不同。
若要理解对象方法,首先需要仔细了解一下JavaScript函数。
JavaScript函数是最棒的
在很多编程语言中,函数和对象通常被视为两样不同的东西。
在JavaScript中,其差别很模糊—JavaScript函数实际上是具有与它关联的可执行代码的对象。
请如此看待普通函数:
functionfunc(x){
alert(x);
}
func(“blah”);
这就是通常在JavaScript中定义函数的方法。
但是,还可以按以下方法定义该函数,您在此创建匿名函数对象,并将它赋给变量func
varfunc=function(x){
alert(x);
};
func(“blah2”);
甚至也可以像下面这样,使用Function构造函数:
varfunc=newFunction(“x”,“alert(x);”);
func(“blah3”);
此示例表明函数实际上只是支持函数调用操作的对象。
最后一个使用Function构造函数来定义函数的方法并不常用,但它展示的可能性非常有趣,因为您可能注意到,该函数的主体正是Function构造函数的String参数。
这意味着,您可以在运行时构造任意函数。
为了进一步演示函数是对象,您可以像对其他任何JavaScript对象一样,在函数中设置或添加属性:
functionsayHi(x){
alert(“Hi,“+x+“!
”);
}
sayHi.text=“HelloWorld!
”;
sayHi[“text2”]=“HelloWorld...again.”;
alert(sayHi[“text”]);//displays“HelloWorld!
”
alert(sayHi.text2);//displays“HelloWorld...again.”
作为对象,函数还可以赋给变量、作为参数传递给其他函数、作为其他函数的值返回,并可以作为对象的属性或数组的元素进行存储等等。
图1提供了这样一个示例。
Figure 1 JavaScript中的函数是最棒的
//assignananonymousfunctiontoavariable
vargreet=function(x){
alert(“Hello,“+x);
};
greet(“MSDNreaders”);
//passingafunctionasanargumenttoanother
functionsquare(x){
returnx*x;
}
functionoperateOn(num,func){
returnfunc(num);
}
//displays256
alert(operateOn(16,square));
//functionsasreturnvalues
functionmakeIncrementer(){
returnfunction(x){returnx+1;};
}
varinc=makeIncrementer();
//displays8
alert(inc(7));
//functionsstoredasarrayelements
vararr=[];
arr[0]=function(x){returnx*x;};
arr[1]=arr[0]
(2);
arr[2]=arr[0](arr[1]);
arr[3]=arr[0](arr[2]);
//displays256
alert(arr[3]);
//functionsasobjectproperties
varobj={“toString”:
function(){return“Thisisanobject.”;}};
//callsobj.toString()
alert(obj);
记住这一点后,向对象添加方法将是很容易的事情:
只需选择名称,然后将函数赋给该名称。
因此,我通过将匿名函数分别赋给相应的方法名称,在对象中定义了三个方法:
varmyDog={
“name”:
“Spot”,
“bark”:
function(){alert(“Woof!
”);},
“displayFullName”:
function(){
alert(this.name+“TheAlphaDog”);
},
“chaseMrPostman”:
function(){
//implementationbeyondthescopeofthisarticle
}
};
myDog.displayFullName();
myDog.bark();//Woof!
C++/C#开发人员应当很熟悉displayFullName函数中使用的“this”关键字—它引用一个对象,通过对象调用方法(使用VisualBasic的开发人员也应当很熟悉它,它在VisualBasic中叫做“Me”)。
因此在上面的示例中,displayFullName中的“this”的值是myDog对象。
但是,“this”的值不是静态的。
通过不同对象调用“this”时,它的值也会更改以便指向相应的对象,如图2所示。
Figure 2 “this”随对象更改而更改
functiondisplayQuote(){
//thevalueof“this”willchange;dependson
//whichobjectitiscalledthrough
alert(this.memorableQuote);
}
varwilliamShakespeare={
“memorableQuote”:
“Itisawisefatherthatknowshisownchild.”,
“sayIt”:
displayQuote
};
varmarkTwain={
“memorableQuote”:
“Golfisagoodwalkspoiled.”,
“sayIt”:
displayQuote
};
varoscarWilde={
“memorableQuote”:
“Truefriendsstabyouinthefront.”
//wecancallthefunctiondisplayQuote
//asamethodofoscarWildewithoutassigningit
//asoscarWilde’smethod.
//”sayIt”:
displayQuote
};
williamShakespeare.sayIt();//true,true
markTwain.sayIt();//hedidn’tknowwheretoplaygolf
//watchthis,eachfunctionhasamethodcall()
//thatallowsthefunctiontobecalledasa
//methodoftheobjectpassedtocall()asan
//argument.
//thislinebelowisequivalenttoassigning
//displayQuotetosayIt,andcallingoscarWilde.sayIt().
displayQuote.call(oscarWilde);//ouch!
图2中的最后一行表示的是将函数作为对象的方法进行调用的另一种方式。
请记住,JavaScript中的函数是对象。
每个函数对象都有一个名为call的方法,它将函数作为第一个参数的方法进行调用。
就是说,作为函数第一个参数传递给call的任何对象都将在函数调用中成为“this”的值。
这一技术对于调用基类构造函数来说非常有用,稍后将对此进行介绍。
有一点需要记住,绝不要调用包含“this”(却没有所属对象)的函数。
否则,将违反全局命名空间,因为在该调用中,“this”将引用全局对象,而这必然会给您的应用程序带来灾难。
例如,下面的脚本将更改JavaScript的全局函数isNaN的行为。
一定不要这样做!
alert(“NaNisNaN:
“+isNaN(NaN));
functionx(){
this.isNaN=function(){
return“notanymore!
”;
};
}
//alert!
!
!
tramplingtheGlobalobject!
!
!
x();
alert(“NaNisNaN:
“+isNaN(NaN));
到这里,我们已经介绍了如何创建对象,包括它的属性和方法。
但如果注意上面的所有代码段,您会发现属性和方法是在对象定义本身中进行硬编码的。
但如果需要更好地控制对象的创建,该怎么做呢?
例如,您可能需要根据某些参数来计算对象的属性值。
或者,可能需要将对象的属性初始化为仅在运行时才能获得的值。
也可能需要创建对象的多个实例(此要求非常常见)。
在C#中,我们使用类来实例化对象实例。
但JavaScript与此不同,因为它没有类。
您将在下一节中看到,您可以充分利用这一情况:
函数在与“new”运算符一起使用时,函数将充当构造函数。
构造函数而不是类
前面提到过,有关JavaScriptOOP的最奇怪的事情是,JavaScript不像C#或C++那样,它没有类。
在C#中,在执行类似下面的操作时:
Dogspot=newDog();
将返回一个对象,该对象是Dog类的实例。
但在JavaScript中,本来就没有类。
与访问类最近似的方法是定义构造函数,如下所示:
functionDogConstructor(name){
this.name=name;
this.respondTo=function(name){
if(this.name==name){
alert(“Woof”);
}
};
}
varspot=newDogConstructor(“Spot”);
spot.respondTo(“Rover”);//nope
spot.respondTo(“Spot”);//yeah!
那么,结果会怎样呢?
暂时忽略DogConstructor函数定义,看一看这一行:
varspot=newDogConstructor(“Spot”);
“new”运算符执行的操作很简单。
首先,它创建一个新的空对象。
然后执行紧随其后的函数调用,将新的空对象设置为该函数中“this”的值。
换句话说,可以认为上面这行包含“new”运算符的代码与下面两行代码的功能相当:
//createanemptyobject
varspot={};
//callthefunctionasamethodoftheemptyobject
DogConstructor.call(spot,“Spot”);
正如在DogConstructor主体中看到的那样,调用此函数将初始化对象,在调用期间关键字“this”将引用此对象。
这样,就可以为对象创建模板!
只要需要创建类似的对象,就可以与构造函数一起调用“new”,返回的结果将是一个完全初始化的对象。
这与类非常相似,不是吗?
实际上,在JavaScript中构造函数的名称通常就是所模拟的类的名称,因此在上面的示例中,可以直接命名构造函数Dog:
//ThinkofthisasclassDog
functionDog(name){
//instancevariable
this.name=name;
//instancemethod?
Hmmm...
this.respondTo=function(name){
if(this.name==name){
alert(“Woof”);
}
};
}
varspot=newDog(“Spot”);
在上面的Dog定义中,我定义了名为name的实例变量。
使用Dog作为其构造函数所创建的每个对象都有它自己的实例变量名称副本(前面提到过,它就是对象词典的条目)。
这就是希望的结果。
毕竟,每个对象都需要它自己的实例变量副本来表示其状态。
但如果看看下一行,就会发现每个Dog实例也都有它自己的respondTo方法副本,这是个浪费;您只需要一个可供各个Dog实例共享的respondTo实例!
通过在Dog以外定义respondTo,可以避免此问题,如下所示:
functionrespondTo(){
//respondTodefinition
}
functionDog(name){
this.name=name;
//attachedthisfunctionasamethodoftheobject
this.respondTo=respondTo;
}
这样,所有Dog实例(即用构造函数Dog创建的所有实例)都可以共享respondTo方法的一个实例。
但随着方法数的增加,维护工作将越来越难。
最后,基本代码中将有很多全局函数,而且随着“类”的增加,事情只会变得更加糟糕(如果它们的方法具有相似的名称,则尤甚)。
但使用原型对象可以更好地解决这个问题,这是下一节的主题。
原型
在使用JavaScript的面向对象编程中,原型对象是个核心概念。
在JavaScript中对象是作为现有示例(即原型)对象的副本而创建的,该名称就来自于这一概念。
此原型对象的任何属性和方法都将显示为从原型的构造函数创建的对象的属性和方法。
可以说,这些对象从其原型继承了属性和方法。
当您创建如下所示的新Dog对象时:
varbuddy=newDog(“Buddy“);
buddy所引用的对象将从它的原型继承属性和方法,尽管仅从这一行可能无法明确判断原型来自哪里。
对象buddy的原型来自构造函数(在这里是函数Dog)的属性。
在JavaScript中,每个函数都有名为“prototype”的属性,用于引用原型对象。
此原型对象又有名为“constructor”的属性,它反过来引用函数本身。
这是一种循环引用,图3更好地说明了这种循环关系。
图3 每个函数的原型都有一个Constructor属性
现在,通过“new”运算符用函数(上面示例中为Dog)创建对象时,所获得的对象将继承Dog.prototype的属性。
在图3中,可以看到Dog.prototype对象有一个回指Dog函数的构造函数属性。
这样,每个Dog对象(从Dog.prototype继承而来)都有一个回指Dog函数的构造函数属性。
图4中的代码证实了这一点。
图5显示了构造函数、原型对象以及用它们创建的对象之间的这一关系。
Figure 4 对象具有其原型的属性
varspot=newDog(“Spot”);
//Dog.prototypeistheprototypeofspot
alert(Dog.prototype.isPrototypeOf(spot));
//spotinheritstheconstructorproperty
//fromDog.prototype
alert(spot.constructor==Dog.prototype.constructor);
alert(spot.constructor==Dog);
//Butconstructorpropertydoesn’tbelong
//tospot.Thelinebelowdisplays“false”
alert(spot.hasOwnProperty(“constructor”));
//TheconstructorpropertybelongstoDog.prototype
//Thelinebelowdisplays“true”
alert(Dog.prototype.hasOwnProperty(“constructor”));
图5 实例继承其原型
某些读者可能已经注意到图4中对hasOwnProperty和isPrototypeOf方法的调用。
这些方法是从哪里来的呢?
它们不是来自Dog.prototype。
实际上,在Dog.prototype和Dog实例中还可以调用其他方法,比如toString、toLocaleString和valueOf,但它们都不来自Dog.prototype。
您会发现,就像.NETFramework中的System.Object充当所有类的最终基类一样,JavaScript中的Object.prototype是所有原型的最终基础原型。
(Object.prototype的原型是null。
)
在此示例中,请记住Dog.prototype是对象。
它是通过调用Object构造函数创建的(尽管它不可见):
Dog.prototype=newObject();
因此,正如Dog实例继承Dog.prototype一样,Dog.prototype继承Object.prototype。
这使得所有Dog实例也继承了O