面向对象程序员JavaScript指南---摘自《Ajax in action》附录B
来源:互联网 发布:淘宝夏季女装 编辑:程序博客网 时间:2024/05/16 17:53
这两天看了四百页的Ajax in Action,全是项目需要,给我最大启发的是书后面的附录,作者介绍了大量的工具,框架和学习JavaScript的入门技巧。所以做了个转载。
书名:Ajax in Action
作者:Dave Crane,Eric Pascarello,Darren James
译者:李琨(dlee),赵东炜(jackyz),王锡平,张祖良(aliang),郑帅,张凯峰(大阿福),徐劲松
----------------------------------------------------------------------------------------------------------------------------------------
1、JavaScript 不是Java
名字蕴含了付么? Java 和JavaScript 的名字巾蕴含着的是大量的市场考虑,而实质相对很少。JavaScript 由" Livescript "改名而来,是Netscape 市场部在最后时刻决定的,现在这个名字已经被广为接受。与一般的理解相反, JavaScript 并不源自C 系列语言,它的思想大多来源于类似Scheme 和Self 的函数式语言(functional language),与Python 也有很多共同之处。很遗憾,它的名字以Java 来命名,从语法样式上很像Java。在一些场合,它的行为与Java 相同,但是在其他很多场合它的行为与Java 并不相同。
表1 总结了JavaScript 的其键特征。
JavaScript 的关键特征及其含义 特征 含义
变量是弱类型的(loose typed) 变最仅仅声明为变量.而不是整数、字符事或者特定类的对象。在JavaScript 中.给同一个变量分配不同的类型是合法的。
代码是动态解释的 与需要预编译的语言(例如Java 、C 、C#) 相比,在运行时,JavaScript 代码以文本形式保存并且在程序运行时解释为机器指令。Web 网站的用户通常可以看到Ajax应用的源代码。而且,这使得通过其他代码来动态生成代码成为了可能,而无需求助于特殊的字节码生成器l
JavaScript函数是普通的对象(Normal Object)
Java 对象的方法与拥有它的对象绑在一起,只能通过该对象来调用。JavaScript 函数可以附加到对象上,使得它们的行为类似于方法。但是它们也可以在其他上下文中调用,而且在运行时附加到其他对象上
JavaScript 对象是基于prototype 的 Java 、C++或者C#对象有一种定义类型:超类、虚的超类(即接口)。这严格定义了它(类的对象或实例)的功能.任何Javascript对象仅仅是一个对象,它仅仅是一个化装的关联数组。prototype 在JavaScript 中可以用来模拟Java 风格的类型.但这只是表面相似
这些特征使JavaScript 能够以不同的方式来使用,同时也为很多古怪的技巧创造了机会,这些技巧值得经验丰富的Lisp 黑客研究一番。如果你是一位聪明并受过专业训练的编码人员,可以利用这些技巧来做些非凡的事情,甚至可能只用几百行代码就做到这些事情。另一方面,如果你白认为确实聪明并受过专业训练,那么你可能很快就会感到羞愧的。
我曾经尝试过几次,最后得出结论,保持事情简单通常是一件好事情。如果你和一个团队共同工作,在技术经理感觉适当的情况下,编码规范或者指南应该可以解决这些问题。
然而,了解这些特征和技巧还有第二个原因:浏览器将在内部使用它们。所以理解内部机理以在调试甚为恶劣的应用时节省时间井减轻痛苦。我发现理解哪些代码的行为与Java 对象不同尤其有帮助,很多相似性仅仅是在外观上的。
继续阅读,会逐渐发现JavaScript 对象实际本质,它们如何由成员字段和函数构成, JavaScript函数实际有什么能力。
2、JavaScript 中的对象
JavaScript 不要求使用对象,甚至不要求使用函数。可以将JavaScript 程序编写成一个文本流,当解释器读取它时直接执行。随着程序逐渐变大,函数和对象成为组织代码的极其有用的方式,我们建议两者都使用。
创建一个新的JavaScript 对象的最简单的方法是调用Object(类)内建的构造函数:
var myObject=new Object();
我们将在后面察创建对象的其他方法,以及关键字new 的真实含义。这里对象myObject初始化为"空“,即它不包含属性或者方法。添加属性和方法非常简单,让我们来看看如何实现。
2.1 创建即时对象
前面曾经提到过, JavaScript 对象本质上是一个关联数组,由以名称作为键的字段和方法组成。在上面修饰了一层类似于C 语言的语法,使得它对于C系列的程序员更加熟悉,但是底层实现可以以其他方式使用。我们以每次一行的方式建立复杂的对象,以我们所认为的方式添加新的变量和函数。
有两种以即时(ad hoc)的方式创建对象的方法。第一种是直接使用JavaScript 来创建对象。
第二种是使用JSON 来创建对象。让我们从普通的旧式JavaScript 技术开始。
1. 使用JavaScript语句
在复杂的代码中,我们可能希望给一些对象属性赋值。JavaScript 对象属性是可读/可写的,可以使用=操作符来赋值。我们将一个属性添加到刚才创建的简单对象上:
myObject.shoeSize="12";
在面向对象语言中,我们需要定义一个类来声明属性shoeSize,否则就会出现编译错误。对与JavaScript 这是不必要的。事实上,仅仅为了强调对象类似于数组的本性,我们也可以使用数组的语法来引用属性:
myObject['shoeSize'j="12";
这种写法除了一个优点(即数组索引是-个JavaScript表达式,这提供了一种运行时反射功能)以外,对于普通的使用来说显得很笨拙,我们将在2.4节回到这个话题。
我们也可以给对象动态添加一个新的函数:
myObject.speakYourShoeSize=function() {
alert("shoe size: "+this.shoeSize);
}
或者借用一个预先定义的函数:
function sayHello{){
alert('hello , my shoeSize is '+this.shoeSize);
}
myObject.sayHello=sayHello;
注意,当分配预先定义的函数时,我们省略了圆括号。如果写成:
myObject.sayHello=sayHello();
那么将执行sayHello 函数,并且用它的返回值来给myObject 的sayHello属性赋值,在这里
是null 。
我们可以将对象附加到其他对象上,从而创建复杂的数据模型等:
var myLibrary=new Object();
myLibrary.books=new Array();
myLibrary.books[0]=new Object();
myLibrary.books[0] .title=" Turnip Cultivation through the Ages";
myLibrary.books[0] .authors=new Array():
var jim=new Object();
jim.name="Jim Brown";
jim. age=9;
myLibrary.books[0] .authors[0]=jim;
这很快就会变得单调乏味。JavaScript提供了一种紧凑的符号,称作JSON。 使用它可以更快地组
装对象图。我们现在就考察一下。
2. 使用JSON
JSON 是语言的一个核心特征,它提供了一种创建数组和对象图(object graph) 的简单机制。为了理解JSON,需要知道JavaScript 数组是如何工作的。我们首先来讨论一些关于它们的基础知识。
JavaScript有一个内建的Array 类,可以使用new 关键字初始化:
myLibrary.books=new Array();
数组有按照数字来分配的值,非常像传统的C 或Java 数组:
myLibrary.book[4]=somePredefinedBook;
数组也可以使用一个键值来关联,像是Java 的Map或者Python 的Dictionary。实际上这可以应用于任何JavaScript对象:
myLibrary.books["BestSeller"]=somePredefinedBook;
这种语法对于微调很奇利,但是作为首选来创建一个大型的数组或者对象就很乏味了。创建一个数字索引的数组的快捷方法是使用方括号,将所有的成员写成一个用逗号分隔的值的列表,就像这样:
myLibrary.books=[predefinedBookl , predefinedBook2 , predefinedBook3]:
为了创建JavaScript对象,我们使用花括号,将每个值写成"键:值"对的形式:
myLibrary.books={
bestSeller: predefinedBookl ,
cookbook: predefinedBook2 ,
spaceFiller : predefinedBook3
} ;
在两种符号中,会忽略额外的空白,这允许我们为了清晰起见对代码做很好的格式处理。键的内部也可以有空白,可以在JSON 符号中使用引号来引用,例如:
"Best Seller" : predefinedBookl ,
我们可以通过嵌套JSON 符号来创建复杂对象层次的单行定义(虽然会是很长的一行);
var myLibrary={
location: "my house" ,
keywords: ["root vegetables" , "turnip", "tedium" ],
books: [ {
title: "Turnip Cultivation through the Ages",
authors: [ { name: "Jim Brown" , age: 9 }, { name: 'Dick Turnip' , age: 312 } ],
publicationDate : "long ago"
},
{
title: "Turnip Cultivation through the Ages , vol. 2" ,
authors: [ { name: "Jim Brown" , age: 35 } ],
publicationDate : new Date(1605 , 11 , 05)
}
]
};
在这里我们给myLibrary 对象分配了3 个属性: location是一个简单字符串;keywords 是一个按数字索引的字符串列表; books 是一个按数字索引的对象列表,每个对象有标题(字符串)、发布日期(代表JavaScript Date 对象的字符串)和作者(对象)列表(数组)。每个作者由name 和age参数来代表。JSON 为我们提供了简练的机制来以单一的途径创建这些信息,否则就会花费很多行代码(以及更多的带宽)。
目光敏锐的读者已经注意到,我们使用JavaScript Date 对象来生成第二本书的发布日期。事实上,我们可以使用任何JavaScript代码来赋值,甚至是自己定义的函数:
function gunpowderPlot(){
return new Date(1605 ,11 , 05);
}
var volNum=2;
var turnipVo12={
title: "Turnip Cultivation through the Ages , vol." + volNum,
authors: [{ name: "Jim Brown", age: 35 }],
publicationDate : gunpowderPlot(),
};
在这里,书的标题使用内嵌的表达式来动态计算,publicationDate 设置为从预先定义函数返回的值。在前面的例子中,我们定义了函数gunpowderPlot( ) ,该函数在对象创建时求值。我们也可以为使用JSON 创建的对象定义成员函数,它在稍后可以通过该对象来调用:
var turnipVo12={
title "Turnip Cultivation through. the Ages , vol. "+volNurn,
authors : [{ name: "J im Brown". age: 35 }],
publicationDate : gunpowderPlot(),
summarize:function(len) {
if (!len) { len=7; }
var summary = this.title+ " by "
+this.authors[0] .name
+" and his cronies is very boring. z";
for (var i = 0; i < len; i++){
summary+=" z" ;
}
alert (summary) ;
}
};
......
turnipVol2.summarize(6);
summarize ( )函数具有标准JavaScript属数的所有特征,例如参数和用关键字this 标识的上下文对象。事实上,一旦对象创建了,它仅仅是另外一个JavaScript 对象,只要我们喜欢,可以混合和匹配使用JavaScript 和JSON 符号。我们可以使用JavaScript 米微调使用JSON 声明的对象:
var numbers={ one:1, two:2 , three:3 };
numbers.five=5;
我们最初使用JSON语法来定义一个对象,然后使用普通的JavaScript添加属性。同样的,我们可以使用JSON 扩展JavaScript所创建的对象:
var cookbook=new Object();
cookbook.pageCount=321;
cookbook.author={
firstName: "Harry" ,
secondName: "Christmas",
birthdate: new Date(1900 , 2 , 29) ,
interests: ["cheese","whistling" ,"history of lighthouse keeping"],
};
通过内建的JavaScript Object和Array类以及JSON 符号,可以创建我们喜欢的任意复杂的对象层次。我们不再需要其他任何东西。JavaScript 也提供了创建对象的方法,为面向对象程序员提供了令人舒适的类定义的相似性,下面考察一下它能为我们提供些什么。
2.2 构造函数、类和原型
在面向对象编程中,我们通常使用希望实例化的类的声明来创建对象。Java 和JavaScript 都
支持new 关键字,允许我们创建预先定义类别的对象的实例,在这里两者是相似的。
在Java中,所有的东西(除了少数的基本类型)都是一个对象,都继承自java.lang.Object类。Java虚拟机对于类、字段和方法具有内建的理解,当我们在Java中声明:
MyObject myObj=new MyObject(argl , arg2);
我们首先声明变量的类型,然后使用相关的构造函数对它实例化。成功的先决条件是类MyObject已经声明并且提供了一个合适的构造函数。
JavaScript 也有对象和类的概念,但是没有内建继承的概念。事实上,每个JavaScript对象是相同基类(一个有能力在运行时将成员字段和函数与自己绑在一起的类〉的实例。所以,有可能在运行时给对象分配任意的属性:
MyJavaScriptObject.completelyNewProperty="something";
可以通过使用一个原型把这种完全自由的状态组织为低层次面向对象开发者更加熟悉的东西。当使用一个特定的函数来构造对象时,原型定义了将自动绑定在对象上的属性和函数。有可能编写基于对象的JavaScript而不使用原型,但是当开发复杂的富客户端应用时,原型在一定程度上为面向对象开发者提供了他们非常想要的规律性和熟悉感。
然后,在JavaScript 中,我们就可以编写一些看起来与Java 声明很相似的东西。
var myObj=new MyObject();
但是我们没有定义类MyObject. 而是定义了一个同名的函数。这里是一个简单的构造函数:
function MyObject(name , size){
this.name=name;
this.size=size;
}
我们随后可以像这样来调用它:
var myObj=new MyObject("tiddles" , "7.5 meters");
alert("size of "+myObj.name+ " is "+myObj.size);
在构造函数中,设置为this 的属性的任何东西随后都可以作为对象的一个成员来使用。我们也许还希望初始化对alert ()的调用,这样tiddlers 可以自己负责告诉我们它有多大。一种通常的习惯做法是在构造函数中声明这个函数。
function MyObject(name , size){
this.name=name;
this.size=size;
this.tellSize=function(){
alert("size of "+this.name+" is "+this.size);
}
var myObj=new Object("tiddles" , "7.5 meters");
myObj.tellSize() ;
这段代码可以工作,但是在两个方面不尽理想。首先,每当创建一个MyObject 的实例时,我们都会创建一个新的函数。作为负责任的Ajax 程序员,我们从来没有忽视过内存泄漏问题。如果找们计划创建很多这样的对象,当然应该避免这种惯用法。其次,我们在这里无意中创建了一个闭包(closure) 一一在这个情况下是无害的一一但是一旦在构造函数中包括了DOM 节点,可以会遇到更加严重的间题。我们将在稍后部分更加详细地考察闭包。现在,让我们考察一下更加安全的替代品:原型(prototype )。
原型是JavaScript 对象的一个属性,在面向对象语言中没有对等物。函数和属性可以与构造函数的原型关联起来。然后原型和new 关键字协同工作,当使用new 调用函数时,函数原型的所有属性和方法会附加到结果对象上。这听起来有点奇怪,但是实际上它很简单:
MyObject.prototype.tellSize=function() {
alert("size of "+this.name+" is "+this.size;
}
var myObj=new MyObject("tiddles" , "7.5 meters");
myObj.tellSize() ;
首先,我们像以前那样声明了构造函数,然后向原型添加函数。当我们创建一个对象实例时,函数附加在上面。关键宇this 在运行时确定为对象的实例,一切都运转良好。
注意这里事件发生的顺序。在声明构造函数之后,我们才能引用原型,对象只继承那些在调用构造函数之前已经添加到原型上的东西。原型可以在两次调用构造函数之间进行修改,并且可以附加任何东西(不仅仅是函数)到原型上:
MyObject.prototype.color="red";
var obj1=new MyObject();
MyObject.prototype.color="blue";
MyObject.prototype.soundEffect="boOOOoing! !";
var obj2=new MyObject();
obj1 将是红色的且没有声音效果,obj2 将是蓝色的且有着令人愉快的声音效果!以这种方式在运行时修改原型通常价值不大。知道发生这种事情是很有用的,但是使用原型来为JavaScript对象定义类似于类的行为,是一条安全和可靠的路径。
有趣的是,也可以扩展某些确定的内建类(即那些由浏览器自身实现和通过 J avaScript暴露出来的类,也称作宿主对象)的原型。让我们考察一下它如何工作。
2.3 扩展内建类
JavaScript 用来嵌入在那些能够向脚本环境暴露自己本地对象(典型地是使用C++或Java 开发)的程序中。这些对象通常描述为内建类或者宿主对象,它们与我们讨论过的用户定义对象某种程度的差别。尽管如此,原型机制也可以与内建类协同工作。在Web 浏览器中, DOM 节点在IE浏览器中不能扩展,但是其他的核心类在所有主要的浏览器中都是可以扩展的。让我们将Array 类作为一个例子并定义一些有用的帮助函数:
Array.prototype.indexOf=function(obj) {
var result=-1;
for (var i = 0; i < this.length; i++) {
if (this[i]==obj) {
result=i;
break;
}
}
return result;
}
这为Array 对象提供了一个额外的函数,它返回一个给定数组中的对象的数字索引,如果数组不包含这个对象就返回-1 。我们可以在这个基础上更进一步,编写一个方便的方法来检查数组是否包含对象:
Array.prototype.contains=function(obj) {
return (this.indexOf(obj)>=0);
}
然后添加另外4个函数,在经过了可选的重复检查之后添加新的成员:
Array.prototype.append=function(obj , nodup) {
if (! (nodup && this.contains(obj)) {
this(this.length)=obj;
}
}
任何在这些函数声明之后创建的Array 对象,无论是使用new 操作符或者作为JSON 表达式的
一部分来创建,都能够使用这些函数:
var numbers=[l , 2 , 3 , 4 , 5];
var got8 = numbers.contains(8);
numbers.append ( "cheese", true) ;
对于用户定义对象的原型而言,它们能够操作在多个对象的创建过程之间,但是我通常建议原型只在程序的开始修改一次,以便避免不必要的混乱,特别是当你在与一个程序员团队共同工作的时候。
当为我们的Ajax应用开发客户端对象模型时,原型可以提供很多东西。一个习惯于C++ 、Java或C#的小心翼翼的对象建模人员不希望定义不同的对象类型,而希望在类型之间实现继承。JavaScript没有提供方便的方法来做这件事情,但是原型在这里也可以派上用场。让我们看看如何做。
2.4 原型的继承
面向对象不仅提供了清晰划分的对象类,也提供了它们之间的结构化的继承层次。经典的例子是Shape 对象,它为计算周长和面积定义了方法,在它之上我们为矩形、正方形、三角形和圆形创建了具体的实现。
继承有一个作用域(scope) 的概念。一个对象的方法或属性的作用域确定了谁可以使用它一一即,它是否是公用的、私有的或者受保护的。
当定义一个领域模型时,作用域和继承是很有用的特征。不幸的是, JavaScript 对这两者都没有在本地实现。尽管如此,这并没有阻止人们去尝试,并己经开发出来了一些相当优雅的解决方案。
Doug Crockford开发了一些灵巧的变通方法,使得在JavaScript对象中能够使用继承和作用域。他所完成的工作毫无疑问是给人印象深刻的,但却太复杂了,无法在这里详细描述。对于不熟悉的读者来说,他的技术所要求的语法有些令人费解。在一个基于团队的项目中,采纳这样的技术应该像采纳Struts 或Tapestry 那样的尺寸和复杂度的Java 框架一样,需要经过深思熟虑。我力劝任何对于这个领域感兴趣的人好好读读Crockford 网站上的文章。
在面向对象领域中,有一种逐渐远离使用复杂的继承,转向使用组合的趋势。通过使用组合,普通的功能移出了对象而转到帮助 (Helper) 类中,帮助类可以作为一个成员附加到任何需要它的类上面。在很多场合,组合可以提供与继承相似的优点, JavaScript 可以完美而充分地支持组合。
我们对于JavaScript 对象短暂旅行的下一站是反射。
2.5 JavaScript对象的反射
在编写代码的正常过程中,程序员对于它正在处理的对象是如何组成的(即它们的属性和方法)有着清晰的了解。然而,在一些情况下,我们需要能够在对对象完全不了解的情况下进行处理,井且在处理它们之前发现它们的属性和方法的性质。例如,如果编写一个日志系统或者一个调试系统,我们可能必须能够处理任意来自外部世界的对象。这个发现的过程称作反射(reflection) ,大多数Java 和.NET 程序员对比应该是很熟悉的。
如果我们希望发现一个JavaScript 对象是否支持一个特定的属性或者方法,我们可以简单地测试它:
if (MyObject.someProperty){
// do something
}
然而,如果MyObject.someProperty赋值为布尔值false、数字0或者特殊值null. 这个测试将会失败.一个更加严格的测试应该这样写:
if (typeof(MyObject.someProperty) != "undefined") {
// do something
}
如果我们关心属性的类型,也可以使用instanceof 操作符。这可以识别出少量基本的内建类型:
if (myObj instanceof Array){
// do something
} else if (myObj instanceof Object) {
// do something else
}
以及任何通过构造函数来定义的类定义:
if (myObj instanceof MyObject) { // do something }
如果你确实喜欢使用instanceof 来测试自定义类,有必要知道两个陷阱(gotcha) 。首先, JSON不支持它一一任何使用JSON 创建的东西要么是一个JavaScript Object,要么是一个Array。其次,内建的对象确实支持存在于它们中间的继承。例如,Function和Array 都是继承自Object,所以测试的顺序是相关的。
如果我们这样写:
function testType(myObj){
if (myObj instanceof Array){
alert("it's an array");
}else if (myObj instanceof Object){
alert("it's an object");
}
testType ( [1 , 2 , 3 , 4]) ;
并且将-个Array 传进代码中,我们将被正确地告知一一是一个Array。另一方面,如果我们这样写:
function testType(myObj) {
if (myObj instanceof Object) {
alert("it's an object");
}else if (myObj instanceof Array) {
alert("it's an array");
}
testType([1 , 2 , 3 , 4]);
那么我们将被古知一一是一个Object,这在技术上也是对的,但是也许不是我们意图得到的结果。
最后,有些时候我们希望无遗漏地发现一个对象的所有属性和函数。我们可以使用简单的for 循环来做这件事:
function MyObject() {
this.color='red' ;
this.flavor='strawberry';
this.azimuth='45 degrees';
this.favoriteDog='collie' ;
}
var myObj=new MyObject();
var debug="discovering.../n” ;
for (var i in myObj) {
debug+= i + " -> " + myObj[i] +"/n" ;
alert (debug) ;
}
这个循环将会执行四次,返回所有设置在构造函数中的值。for 循环的语法在内建的对象上也能
工作——当对DOM 节点执行时,简单的debug 循环就可以产生非常大的警告框!可以使用这个技术来开发递归的ObjectViewer 的用户界面。
我们还要提及传统面向对象语言的另一个特征一一虚类或者接口,下一篇我们就来考察一下。
本文来自CSDN博客,转载请标明出处:file:///I:/面向对象程序员的JavaScript指南(1)%20-%20baiwen1979的专栏%20-%20CSDN博客.htm2.6 接口和"鸭子类型"
在软件开发中,有很多时候我们希望指定某种行为而不提供具体的实现。例如,在Shape 对象被正方形、圆形等对象子类化(subclassed) 的情况下,我们知道将无法得到一个不是某种特定类型的形状。Shape 对象的基本概念是对于通用属性的方便的抽象,而没有真实世界中的等同物。
C++的虚类或者Java 的接口为我们在代码中定义这些概念提供了必要的机制。我们经常说接口在不同的软件组件之间定义了一个契约。有了这个契约, Shape 处理库的作者不必去考虑特定的实现,新的Shape 实现的作者不必去考虑任何库代码的内部实现或者任何同一接口的现有实现。
接口提供了好的概念分离,并且支撑了很多的设计模式。如果在Ajax 中使用设计模式,我们希理使用接口。JavaScript 没有正式的接口概念,那么我们如何来做呢?
最简单的方法是非正式地定义契约,井且在接口的每一端简单地依赖于开发者,明白他们正在做什么。Dave Thomas 给这个方法起了一个迷人的名字"鸭子类型" (duck typing) 一一一如果它走起来像鸭子,叫起来也像鸭子,那么它就是一只鸭子。对于Shape 接口是类似的,如果它能够计算面积和周长,那么它就是-一种形状。
假设我们希望将两个形状的面积加在一起。在Java 中,我们可以编写:
public double addArea5(Shape s1,Shape s2){
return s1.getArea() + s2.getArea();
}
方法签名明确禁止我们传递进除了形状以外的任何其他东西。于是在方法体内,我们知道我们将会遵守这个契约。在JavaScript 中,方法的参数没有类型,因此我们没有这样的保证:
function addArea5(s1,s2) {
return s1.getArea() + s2.getArea();
}
如果任何一个对象没有附加的函数getArea (),我们将会得到-个JavaScript 错误。我们可以在调用它之前检查函数是否存在:
function hasArea(obj){
return obj && obj.getArea && obj.getArea instanceof Function;
}
并且修改函数来使用这个检查:
function addAreas(s1 , s2){
var total=null:
if (hasArea(s1) && hasArea(s2) {
total=s1.getArea()+s2.getArea() ;
}
return total;
}
实际上,使用JavaScript 反射,我们可以编写一个通用的函数来检查对象是否有一个特定名称的函数:
function implements(obj , funcName) {
return obj && obj[funcName] && obj[funcName] instanceof Function;
}
或者,我们还可以将它附加到Object类的原型上:
Object.prototype.implements=function(funcName) {
return this && this[funcName] && this[funcName] instanceof Function;
}
这允许我们使用名称来检查特定的函数:
function hasArea(obj) {
return obj.implements("getArea");
}
甚至还可以测试对象是否遵守了一个完整的接口:
function isShape(obj) {
return obj.implements("getArea") && obj.implements("getPerimeter");
}
这给我们带来了一定程度的安全性,虽然仍然不如我们在Java 中获得的一样多。例如,一个恶意对象(rogue object) 可以将getArea ()实现为返回一个字符串而不是一个数字值。除非调用它,否则我们无法知道JavaScript 函数的返回值类型,因为JavaScript 函数没有预先定义的类型(我们甚至可以编写一个函数,在每周工作日返回数字,而在周末返回字符串)。编写一套简单的测试函数来检查返回类型是很容易的,例如:
function isNum(arg) {
return parseFloat(arg) !=NaN;
}
NaN是"非数" (not a number) 的简写,是处理数字格式错误的一个特殊的JavaScript 变量。如果字符串以数字部分开始,这个函数实际上也会返回true. parseFloat() 和它的堂兄弟parseInt ()在可能的地方会竭尽全力抽取可以识别的数字。parseFloat ("64hectares") 将会得到64 ,而不是NaN。
我们可以更进一步来增强addAreas ()函数:
function addAreas(s1,s2) (
var total=null;
if (hasArea(s1) && hasArea(s2)) {
var a1=s1.getArea();
var a2=s2.getArea();
if (isNum(a1) && isNum(a2)) {
total=parseFloat(a1)+parseFloat(a2);
}
}
return total;
}
我在两个参数上调用parseFloat(),以便正确地去掉字符串中的非数字部分。如果s1 返回值32 , s2 返回值64 hectares ,那么addreas ()将会返回96 。如果我不使用parseFloat ,就将得到容易被误解的值3264 hectares!
概括来说,鸭子类型使事情保持简单,但是要求你相信开发团队能够明白所有的细节。鸭子类型在Ruby 社区中很流行,他们通常是一群非常聪明的家伙。随着一个人从单个作者或者小型联系紧密的团队转移到大型的(包括分散的小团队)项目中,这种信任会不可避免地削弱。如果你希望在鸭子类型之上为代码添加一些检查和平衡,本节为你展示了应该从哪里开始。
我们已经从对象的角度考察了这种语言。现在让我们深入一些来考察那些散落在各处的函数,看看它们实际上是什么。
3、方法和函数
在前面几节和本文章的其余部分我们定义了函数,并且调用它们。一名Java 或者C#程序员可能会假设它们类似于方法,是通过看起来有点古怪的语法来定义的。在本节中,我们将对函数进行更多的剖析,看看可以对它们做些什么。
3.1 函数是一等公民
函数有点像Java 的方法。调用时,也有参数和返回值,但是有一个关键的区别: Java 方法天生捆绑在定义它的类上,不能与类脱离开而存在; JavaScript 函数是自由浮动的实体,自身就可以作为正常对象(静态的Java方法位于这两者之间一一它们并没有捆绑在任何对象实例上,但是仍然捆绑在类的定义上)。
一名兢兢业业的C系列语言程序员可能会认为"啊,那么它看起来像是C++ 中的一个函数指针。" 确实是这样,但是这还不是函数的全部。
在JavaScript 中, Function 是咱一个内建的对象类型。就像期待的那样,它包含可执行的代码,可以调用,但是它也是Object 类的子孙,并且可以做JavaScript 对象可以做的任何事情,例如使用名称来保存属性。Function 对象上很可能(井且非常普遍)附加其他的Function 对象作为它的方法。
我们已经看到如何获得Function 对象的引用。更为通常的是,我们希望在单独一行中引用一个函数并且调用它,例如:
var result = MyObject.doSomething(x , y , z)
然而, Function 是第一等的对象,它也可以通过call ()方法(以及它的近亲apply()方法)来执行:
var result = MyObject.doSomething.call(MyOtherObject , x ,y , z)
或者甚至是:
var result = MyObject['doSomething'] .call(MyOtherObject , x ,y , z)
Function.call()的第一个参数是在调用期间作为函数上下文使用的对象,随后的参数作为函数调用的参数。apply( )的工作方式略微不同,其中第二个参数是一个传递给函数调用的参数数组,允许以编辑方式调用那些参数列表长度不确定的函数,这带来了巨大的灵活性。
这里值得指出的是,JavaScript 函数的参数列表的长度不固定。使用比声明更多或者更少的参数来调用户一个Java 或者C#方法将会产生编译期错误。而JavaScript仅仅忽略任何额外的参数,并且给缺少的参数赋值undefined。一个特别智能的函数可以通过 arguments属性查询它自己的参数列表,并且为缺少的值分配明智的默认值,抛出一个异常或者采取任何其他的补救措施。这个特征可以通过将获取和设置方法组合在单个函数中来演示,例如:
function area(value) {
if (value) {
this.area=value;
}
return this.area;
}
如果简单地调用area() ,那么value 是未定义的,所以没有发生赋值,函数作为getter方法来使用。如果传入了一个值,函数就作为setter方法来使用。这种技术被Mike Foster的x库广泛使用。所以,如果你计划使用这个库,你很快就会熟悉这个习惯用法。
尽管如此,当我们对函数作为第一等对象的独立性加以利用时,它才变得真正有趣起来。
3.2 向对象附加函数
作为一种函数式语言,JavaScript 允许脱离任何对象来定义函数,例如:
function doSomething(x , y , z) { ... }
函数还可以使用内嵌的方式来定义:
var doSomething=function(x , y , z) { ... }
作为向面向对象方法的妥协,函数可以附加到对象上,这使得函数有了Java 或者C#的方法的"外
表"。实现方法不止一种。
我们可以将预先定义的函数附加到预先定义的对象上(在这种情况下,只有该对象可以调用这个函数,而不是从相同原型继承的任何其他对象都可以):
myObj.doSomethingNew=doSomething;
myObj.doSomethingNew(x , y , z) ;
我们也可以添加函数使得类的每一个实例都能访问它们:在构造函数中将函数(预先定义的或者以内嵌方式声明的)添加到新的对象上,就像在2.2 节看到的那样:或者将函数附加到原型上。完成之后,函数仍然不是非常牢固地附加在对象上,我们下面会看到这一点。
3.3 从其他对象借用函数
函数成为一等对象,极大地改变了语言的能力。而且,当为GUI 事件编写处理代码的时候,理解这些改变是很重要的,所以大多数Ajax 程序员觉得有必要理解它。
那么这些新的能力是什么呢? 首先,一个对象可以借用另外一个对象的函数,并且通过自身来调用它。让我们定义一个类来表示分类学意义上的树:
function Tree(name,leaf,bark){
this.name=name:
this.leaf=leaf;
this.bark=bark;
}
下面,我们将添加一个函数,这个函数提供了对树的简短描述:
Tree.prototype.describe = function() {
return this.name+": leaf="+this.leaf+" , bark="+this.bark;
}
如果现在实例化一个Tree 对象,并且请求它描述自己,将得到一个结果可以预见的响应:
var Beech=new Tree ("Beech" , "green , serrated edge" , "smooth");
alert(Beech.describe()) ;
警告相互将显示文本: Beech: leaf=green , serrated edge,bark=smooth。到目前为止一切顺利。现在让我们定义一个类来表示狗:
function Dog(name , bark) {
this.name=name;
this.bark=bark;
}
并为Dog 类创建一个实例:
var Snowy=new Dog ( "Snowy" , "wau! wau!");
Snowy 希理向我们展示它的叫声,尽管我们已经为它定义了叫声,但是并没有定义表达函数。然而,它可以劫持Tree类的函数:
var tmpFunc=Beech.describe;
tmpFunc.call(Snowy) ;
记住, Function.call() 的第一个参数是上下文对象,即特殊变量this 将被确定为的对象。前面的代码将生成一个警告框,显示文本Snowy: leaf=undefined, bark=wau! waul 。很好,这比可怜的小狗什么都没有要好。
那么,这里发生了什么事情? 狗怎么能够调用实际上属于树的函数呢?答案是函数并不属于那棵树。尽管其中插入了this 引用,将函数赋给Tree 原型形成的绑定仅仅是因为这样能够使用更短的符号MyTree.describe()来调用它。在内部实现中,函数保存为一段每次调用时就会求值的文本,因此this 的含义在一次调用和下一次调用中并不相同。
借用函数是我们可以在自己的代码中使用的一个灵巧的技巧,但是在产品级代码中,我们更乐意看到某人自愿为Snowy 实现一个bark() 方法。讨论这个行为真正的原因是当你在编写事件处理代码时, Web 浏览器将以后台的方式自动帮你做这些事情。
3.4 Ajax 事件处理和函数上下文
对于鼠标和键盘事件的特殊种类来说,Ajax事件处理函数和大多数GUI 工具包语言(GUI toolkit language) 中的事件处理函数差不多是一样的,我们的例子使用了OnClick 处理函数,它在鼠标在一个可视的元素上点击的时候触发。对于DHTML 事件处理的完整讨论超出了本文的范围,但是让我们在这里花一点时间,关注一个经常使粗心的开发者犯错的特定问题。
事件处理函数要么当作HTML 标记的一部分来声明,例如:
<div id='myDiv' onclick='alert:alert(this.id) '></div>
要么使用编程方式来声明,例如:
function clickHandler() { alert(this.id); }
myDiv.onclick=clickHandler;
注意,在编程方式的情况下,我们传递的是一个对Function 对象的引用(在clickHandler后面没有())。当在HTML 中声明函数时,我们有效地以内嵌方式声明了个匿名函数,等同于:
myDiv.onclick=function(){ alert(this.id); }
注意,在两种情况下,都没有为函数分配参数,也没有任何方式可以伴随鼠标的点击传递参数。然而,当点击DOM 元素的时候. Event 对象作为了函数调用的参数,元素本身作为上下文对象。知道这一点可以大大减少麻烦和困惑,特别是当你在编写面向对象的代码时。混乱的关键之源在于DOM 节点总是作为上下文来传递,甚至在函数附加到一个不同对象的原型上的时候。在下面的例子中,我们定义了一个简单的对象,该对象带有一个它所知道的可视 GUI 元素的事件处理函数。我们可以把这个对象看作是MVC 术语中的模型,事件处理函数看作是控制器,DOM 元素是视图。
function MyObj (id , div) {
this.id=id;
this.div=div;
this.div.onclick=this.clickHandler;
}
构造函数接受一个内部的ID和一个分配了 onclick处理函数的 DOM元素作为参数。我们像下面这样定义事件处理函数:
MyObj.prototype.clickHandler=Function(event){
alert (this. id) ;
}
于是,当点击 GUI元素的时候,它将显示那个对象的 ID,这个判断对吗?事实上,它并没有这样做,因为 MyObj.clickHandler函数将会被浏览器借用(就像在前面一节中任性的小狗从树对象上借用了一个方法一样),并且在那个元素的上下文中调用,而不是在模型对象的上下文中。因为元素碰巧也有一个内建的 id属性,它将会显示一个值,并且依赖于命名约定,这个值甚至可以与模型对象的ID相同,这会使你的误解持续到未来的某个时间。如果希望事件处理函数引用它附加到的模型对象,我们高要使用另外一种方式将那个对象的引用传递进来。我遇到过两种习惯做法可以做这件事。从我的观点来看,一种做法明显要比另一种优越,但是我使用另一种方法编程已经很多年,它一样可以工作。本文的一个目标是为习惯上采用的模式(和反模式)命名,所以两种方法我们在这里都会介绍。
1.使用名称引用模型
在这种解决方案中,我们给模型对象的每一个实例分配全局唯一的ID,并且维护一个通过ID来引用的这些对象的全局数组。假设我们得到了一个对 DOM元素的引用,随后就可以通过使用ID的一部分作为查找数组( lookup array)的键,来引用它的模型对象。图 1展示了这种策略。
在这种方法中,为每一个元素生成唯一的ID 是一种系统开销,但是ID 的生成可以自动地完成。例如,如果在Web 服务器上生成代码,我们可以使用数组长度作为这个键的一部分,也可以使用数据库的主键作为键。作为一个简单的例子,我们创建了一个类型为MyObj 的对象,它有一个可以点击的标题栏,调用函数rnyObj.foo()。
全局数组如下:
var MyObjects=new Array(};
构造函数如下,它在数组中注册模型对象:
function MyObj(id) {
this.uid=id;
MyObjects[this.uid]=this;
this.render() ;
}
MyObj 对象的方法如下,其中有一些有意思的操作。我们希望当点击标题栏的时候调用这个方法:
MyObj.prototype.foo=function(){
alert( 'foooo!!!'+this.uid);
}
对象的render()方法如下,它创建不同的DOM节点:
MyObj.prototype.render=function(){
this.body=document.createElement("div");
this.body.id=this.uid+"_body";
this.titleBar=document.createElement("div") ;
this.titleBar.id=this.uid+'_titleBar";
this.titleBar.onclick=fooEventHandler;
}
当在这个模型对象的视图中构建任何DOM 节点时,我们为它们分配了一个包含了模型对象ID的ID 值。
注意,我们引用了函数FooEventHandler(),并且将它设置为标题栏DOM 元素的onclick属性:
function fooEventHandler(event){
var modelObj=getMyObj(this.id);
if (modelObj) { modelObj.foo(); }
}
事件处理函数需要找到MyObj 的实例,以便调用它的foo( )方法。我们提供了一个发现方法:
function getMyObj(id) {
var key=id.split("_")[0];
return MyObjects[key];
}
事件处理函数有一个到DOM 节点的引用(即this) ,可以从它的id 属性中抽取出个键,用来从全局数组获得模型对象。
要介绍的就是这些。我使用名称引用模型(Reference Model By Name) 的方法好几年了,一直感觉不错,这方法很好使,但是还有另一个更加简单、更加清晰的方法,这种方法不会给你的DOM 树加上很多冗长的ID。 (实际上,我从未确定这究竟是好还是不好。它肯定浪费了内存,但是也使得在Mozilla DOM 检查器中进行调试非常容易。)
2. 向DOM 节点附加模型
在DOM 事件处理的第二种方法中,所有的工作都使用对象引用来完成,而不是使用字符串,也不再需要全局查找数组。这是本文所采用的方法,图2 展示了这种方法。
这种方法相当大地简化了事件处理函数的工作。模型对象的构造函数不再需要专门的ID处理. foo()方法和前面定义的一样。当构造DOM 节点时,我们发掘了JavaScript 可以附加任意属性到任意对象上的动态能力,并且将模型对象直接附加在接收事件的DOM 节点上面:
MyObj.prototype.createView=function() {
this.body=document.createElement("div") ;
this.body.modelOb)=this;
this.titleBar=document.createElement("div");
this.titleBar.modelObj=this;
this.titleBar.onclick=FooEventHandler;
}
当编写事件处理函数的时候,我们可以获得一个到后端模型的直接引用:
function fooEventHandler(event){
var modelObj=this.modelObj;
if (modelObj) { modelObj.foo(); }
}
没有发现函数,也没有全局查找表一一它是非常简单的。
然而,最后还有一句警告。当使用这种模式的时候,我们在DOM 变量和非DOM 变量之间创建了循环引用。根据Web 浏览器有关的传言,在目前某些流行的浏览器中,这对于垃圾收集是很不利的。如果正确使用这种模式,是可以避免内存开销的,但是我还是建议在实现这种模式 (即向DOM 节点附加模型)之前学会如何降低内存使用率。
理解JavaScript 函数如何定义它的上下文,可以帮助我们为浏览器事件模型开发一种优雅的、可重用的解决方案。函数在上下文之间切换的能力可能在最初会把人摘糊涂,但是理解其背后的模型可以帮助我们更好地使用它。
关于JavaScript 函数我们需要理解的最后一点,是语言创建闭包的能力。Java 和C#没有闭包的概念,但是一些Java 和.NET 脚本语言(例如Groovy 和Boo) 支持闭包,并且C# 2.0 也将会支持闭包。我们来考察一下什么是闭包,以及如何使用它们。
3.5 JavaScript 中的闭包
Function 对象本身是不完整的一一为了调用它,我们需要传进一个上下文对象以及一组参数(可能是一个空的集合)。在最简单的情况下,闭包可以看作是捆绑了运行所需所有资源的Function 对象。闭包在JavaScript 中是隐式而非显式创建的。没有构造函数new Closure ( ) ,也没有方法来得到闭包对象的句柄。创建闭包就像在代码块中(例如在另一个函数中)声明函数并且使得该函数在代码块之外可以获得一样简单。
这从概念上听起来有点怪异,但是当我们考察一个例子的时候,它是足够简单的。我们定义一个简单对象来代表一个机器人,并且记录每个机器人创建时的系统时钟时间。我们可以像这样编写构造函数:
function Robot(){
var createTime=new Date();
this.getAge=function(){ //在函数中声明或定义另一个函数即创建了一个闭包
var now=new Date();
var age=now-createTime;
return age;
}
}
(所有的机器人都是相同的,所以我们不必费事通过构造函数给它们分配名称或者任何其他东西。)通常,我们将createTime作为成员属性记录下来,即这样写:
this.createTime=new Date();
但是在这里我们故意将它创建为本地变量(var createTime = new Date()),其作用域限制在调用它的块中(即构造函数中)。在构造函数的第二行,我们定义了函数getAge()。注意,这里我们在一个函数中定义了另一个函数,内部函数使用的本地变量createTime属于外部函数的作用域。通过做这件事(没有做任何其他事情),我们实际上创建了闭包。如果我们定义了一个机器人,在页面加载完成的时刻询问它的"年龄"。
var robbie=new Robot();
window.onload=function(){
alert(robbie.getAge());
}
它可以工作并且给了我们一个在10~50 毫秒之间的值,即脚本第一次执行和页面加载完成之间的时间差。尽管我们已经将createTime声明为构造函数作用域的本地变量,只要仍然要引用机器人Robbie,它就不能被垃圾收集,因为它绑定在了闭包中。
闭包仅仅当内部函数创建在外部函数之内的时候才可以工作。如果我们重构这段代码来预先定义getAge 函数,并且在所有的机器人实例之间共享它,就像这样:
function Robot() {
var createTime=new Date();
this.getAge=roboAge;
}
function roboAge(){
var now=new Date();
var age=now-createTime;
return age;
}
那么就没有创建闭包,且我们得到了一个错误信息,告诉我们createTime没有定义。创建闭包非常容易,以致于太容易意外地创建。因为闭包与本地变量绑在一起,使得它们不能被垃圾器收集。例如,如果也以这种方式捕获DOM 节点,那么无意中创建的闭包随着时间的延长将会造成严重的内存泄漏。
创建闭包的最常见场合,是将事件处理回调函数绑定到事件源上。就像在3.4节中讨论的,回调函数由使用一个上下文和一组有时候没有什么用处的参数来调用(回调函数的参数是开发者无法指定的,所以有时候没有什么用。)。我们介绍了一种用来将额外的引用(模型对象)附加到生成事件的DOM 元素上的模式,允许通过DOM 元素来获得模型。闭包提供了一种做这件事情的替代方法,就像这里演示的那样:
MyObj.prototype.createView=function(){
this.titleBar=document.createElement("div") ;
var modelObj=this; //这里创建了闭包
this.titleBar.onclick=function() {
fooEventHandler.call(modelObj) ;
}
}
我们定义的匿名的onclick 处理函数给本地声明的变量modelObj 加了一个引用,于是在它的周围创建了闭包,允许调用函数时确定modelObj 的值。注意闭包只能确定本地变量的值,而不是那些通过this引用的变量。
我们在ContentLoader(一个封装了Http网络功能的对象) 对象中使用了这种方法,因为IE中提供的onreadystatechange回调函数返回window 对象作为函数上下文。因为window 是在全局范围内定义的,我们没有办法知道是哪一个ContentLoader 对象的readyState 发生了变化,除非通过闭包传递一个到相关加载对象的引用。
我对水平一般的Ajax 程序员的建议是:如果有替代方法,就避免使用闭包。如果使用原型来给自定义对象类型分配函数,那么你就不会重复创建函数,也不会创建闭包。让我们重写Robot类来遵循这个建议:
function Robot() {
this.createTime=new Date();
}
Robot.prototype.getAge=function() {
var now=new Date();
var age=now-this.createTime;
return age;
}
函数getAge()只定义一次,并且因为它附加在原型上,可以被创建的所有Robot 对象访问。闭包有它们的用处,但是最好把它们看作是一种高级的技术。如果你确实希望更深入地探索闭包,请看5.资源相关内容。
4、小结
在本文中,我们带领你领略了JavaScript 语言的一些奇怪和更加有趣的特征。我们有两个目的:首先是展示这种语言强大的表现能力;其次是为粗心者指出几个陷阱,其中以面向对象风格的方式来思考会得到不理想甚至是危险的代码。
我们考察了JavaScript 对于对象的支持和Object 类与Array 类之间的相似性。我们还看到几种分别使用JSON、构造函数以及原型的概念来实例化JavaScript 对象的方法。在这个过程巾中,我们讨论了如何在JavaScript 中以一种与语言"合作而不是对着干"的方式来处理面向对象的概念,例如继承和接口。
在对JavaScript Function 对象的探索中,我们看到了函数如何独立于任何它们所赋予的对象而存在,甚至如何在对象之间借用或交换。我们使用这些知识来更好地理解对于JavaScript 事件模型。最后我们考察了闭包,看到了一些通常的编程习惯如何无意中创建了闭包,潜在地导致了内存泄漏。
与Java或者C#相比, JavaScript 提供了充分的灵活性和空间,可以为这种语言开发个性化的风格和方法。这对于其他程序员来说是好事,因为对自己所做的事情能了如指掌。当在团队中工作时,这也可能会带来问题,但是这些问题可以通过共享代码约定或者编程风格来缓解。
一旦理解了JavaScript 的工作原理,对你而言它就成为一种令人非常愉快的语言。如果你是从面向对象背景转向Ajax开发,我们希望本文可以帮助你跨越这道鸿沟。
5、资源
相对于Web 浏览器编程,关于JavaScript 语言的书很少。David Flanagan 的JavaScript: The Definitive Guide (O'Reilly 2001年出版)是一本权威著作。但这本书有点旧。更新的一本好书是Nicholas Zaka 的Professional JavaScript for 胁b Developers (Wrox 2004 年出版) ,它也很好地全面描述了这种语言,并且覆盖了该语言的一些更新发展。
在Web 上, Doug Crockford 讨论了JavaScript 的面向对象开发方法,例如为类创建私有的成员( www.crockford.com/javascript/private.html) 以及继承( www.crockford.com/javascript/inheritance.
html ) 。 Peter-Paul Koch 的Quirksmode 网站(http://quirksmode.org) 也讨论了很多这种语言的更加细微的知识点。Jim Ley 对于JavaScript 中闭包的讨论可以在http://jibbering.com/faq/faq_notes/closures.html 中找到。
Mike Foster的x库可以在www.cross-browser.com找到。
- 面向对象程序员JavaScript指南---摘自《Ajax in action》附录B
- Ajax In Action 附录B B1
- Ajax In Action 附录B 2.2
- Ajax In Action 附录B 2.6
- Ajax In Action 附录B 3.1
- Ajax In Action 附录 B 3.4
- Ajax In Action 附录B 3.5
- ajax in action——javascript面向对象编程(上)
- 面向对象程序员JavaScript指南
- Ajax In Action 附录 B2.1
- Ajax In Action 附录 B2.3
- Ajax In Action 附录 B2.4
- Ajax In Action 附录 B2.5
- Ajax In Action 附录 B3.2
- Ajax In Action 附录 B3.3
- 面向对象程序员JavaScript指南(2)
- 面向对象程序员的JavaScript指南(1)
- JavaScript面向对象编程指南
- Java中ArrayList和LinkedList区别
- vc++编程
- 深入探索c/c++函数(2)---普通成员函数调用的基本过程
- Java类加载原理及类加载器
- 筛法求欧拉函数值
- 面向对象程序员JavaScript指南---摘自《Ajax in action》附录B
- 我的百度博客
- 2010-03-16
- 【工具软件】文本编辑器 Top 6
- Java操作(DOM、SAX、JDOM、DOM4J)xml方式的四种比较与详解
- Spring Annotation详解 --- IOC篇
- 1874: Relatives欧拉函数
- C# 操作Access 郁闷的%*
- Java中通过缓冲区提高I/O系能