1、JavaScript 不是Java
名字蕴含了付么? Java 和JavaScript 的名字巾蕴含着的是大量的市场考虑,而实质相对很少。JavaScript 由" Livescript "改名而来,是Netscape 市场部在最后时刻决定的,现在这个名字已经被广为接受。与一般的理解相反, JavaScript 并不源自C 系列语言,它的思想大多来源于类似Scheme 和Self 的函数式语言(functional language),与Python 也有很多共同之处。很遗憾,它的名字以Java 来命名,从语法样式上很像Java。在一些场合,它的行为与Java 相同,但是在其他很多场合它的行为与Java 并不相同。
表1 总结了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 的用户界面。
我们还要提及传统面向对象语言的另一个特征一一虚类或者接口,下一篇我们就来考察一下。
如对本文有疑问,请提交到交流论坛,广大热心网友会为你解答!! 点击进入论坛