跨越边界: JavaScript 语言特性研究编程语言中的“丑小鸭” |
|
级别: 初级
Bruce Tate ([email protected]), 总裁, RapidRed
2007 年 1 月 18 日
JavaScript 常被人们认为是编程语言中无足轻重的一员。这种观点的形成可以“归功”于其开发工具、复杂且不一致的面向 HTML 页面的文档对象模型以及不一致的浏览器实现。但 JavaScript 绝对不仅仅是一个玩具这么简单。在本文中,Bruce Tate 向您介绍了 JavaScript 的语言特性。
几乎每个 Web 开发人员都曾有过诅咒 JavaScript 的经历。这个备受争议的语言受累于其复杂的称为文档对象模型 (DOM)的编程模型、糟糕的实现和调试工具以及不一致的浏览器实现。直到最近,很多开发人员还认为 Javascript 从最好的方面说是无可避免之灾祸,从最坏的方面说不过是一种玩具罢了。
然而 JavaScript 现在开始日益重要起来,而且成为了广泛应用于 Web 开发的脚本语言。JavaScript 的复苏使一些业界领袖人物也不得不开始重新审视这种编程语言。诸如 Ajax (Asynchronous JavaScript + XML) 这样的编程技术让 Web 网页更加迷人。而完整的 Web 开发框架,比如 Apache Cocoon,则让 JavaScript 的应用越来越多,使其不只限于是一种用于制作 Web 页面的简单脚本。JavaScript 的一种称为 ActionScript 的派生物也推动了 Macromedia 的 Flash 客户端框架的发展。运行在 JVM 上的实现 Rhino 让 JavaScript 成为了 Java™ 开发人员所首选的一类脚本语言(参见 参考资料)。
我的好友兼同事 Stuart Halloway 是 Ajax 方面的专家,曾在其教授的 JavaScript 课程中做过这样的开场白:“到 2011 年,JavaScript 将被公认为是一种拥有开发现代应用程序所需的一整套新特性的语言” 。他继而介绍说 JavaScript 程序要比类似的 Java 程序紧密十倍,并继续展示了使其之所以如此的一些语言特性。
|
在这篇文章中,我将带您探究 JavaScript 的一些特性,看看这些特性如何让它如此具有吸引力:
- 高阶函数: 一个高阶函数可以将函数作为参数,也可以返回一个函数。此特性让 JavaScript 程序员可以用 Java 语言所不能提供的方法来操纵函数。
- 动态类型:通过延迟绑定,JavaScript 可以更准确和更灵活。
- 灵活的对象模型:JavaScript 的对象模型使用一种相对不常见的方式进行继承 —— 称为原型 —— 而不是 Java 语言中更常见的基于类的对象模型。
您可能已经熟悉动态类型模型、高阶函数形式的函数式编程以及开放对象模型这些概念,因为我在其他的跨越边界 系列文章中已经作过相关的介绍。如果您从未进行过任何正式的 JavaScript 开发,您很可能会认为这些特性属于非常复杂的语言,例如 Python、Lisp、Smalltalk 和 Haskell,而绝非像 JavaScript 这样的语言所能提供的。因此,我将用实际的代码示例来说明这些概念。
立即开始
您无需设置 JavaScript。如果您可以在浏览器中阅读此篇文章,就证明您已经准备就绪了。本文包含的所有编程示例都可以在大多数浏览器内运行。我使用的是 Firefox。
用在 <script type='text/javascript'>
和 </script>
标记之间所包含的 JavaScript 加载简单的 Web 页面。清单 1 可以显示 Hello, World 文本:
清单 1. Hello, world
<script type='text/javascript'> alert('Hello, World.') </script> |
要运行此代码,只需创建一个名为 example1.html 的文件。将清单 1 的代码复制到该文件内,并在浏览器中加载此文件(参看 下载 部分以获得本文使用的所有示例 HTML 文件)。注意到每次重载此页面时,该代码都会立即执行。
alert
是个函数调用,只有一个字符串作为参数。图 1 显示了由清单 1 中的代码弹出的警告框,显示文本 “Hello, World”。如果代码在 HTML body 之内(目前并未指定任何 body,但浏览器能接受不规则的 HTML,并且整个页面都默然作为一个 body 被处理)。页面一旦加载,JavaScript 就会立即执行。
图 1. Hello, world
如果要延迟执行,可以在 HTML <head>
元素声明 JavaScript 函数,如清单 2 所示:
清单 2. 延迟执行
<head> <script type='text/javascript'> function hello() { alert('Hello, World.') } </script> </head> <body> <button onclick="hello();">Say Hello</button> </body> |
将清单 2 中的代码输入到一个 HTML 文件,在浏览器内加载该文件,单击 Say Hello 按钮,结果如图 2 所示:
图 2. 延迟执行
|
|
高阶函数
从 清单 2,可以大致体会到一些 JavaScript 在操纵函数方面的能力。将函数名称传递给 HTML button
标记并利用 HTML 的内置事件模型。使用 JavaScript 时,我会经常在变量或数组中存储函数(在本文后面的 对象模型 一节,您会看到 JavaScript 对象模型策略大量使用了此技巧)。例如,查看一下清单 3:
清单 3. 用变量操纵函数
<head> <script type='text/javascript'> hot = function hot() { alert('Sweat.') } cold = function cold() { alert('Shiver.') } function swap() { temp = hot hot = cold cold = temp alert('Swapped.') } </script> </head> <body> <button onclick="hot();">Hot</button> <button onclick="cold();">Cold</button> <button onclick="swap();">Swap</button> </body> |
函数是 JavaScript 中的一类对象,可以自由地操纵它们。首先我声明两个函数:hot
和 cold
。并分别在不同的变量存储它们。单击 Hot 或 Cold 按钮会调用对应的函数,生成一个告警。接下来,声明另一个函数用来交换 Hot 和 Cold 按钮的值,将此函数与第三个按钮关联,该按钮显示如图 3 所示的告警:
图 3. 操纵函数
这个例子说明可以像处理其他变量一样处理函数。C 开发人员很容易将此概念看作是函数指针 功能,但 JavaScript 的高阶函数的功能更为强大。该特性让 JavaScript 程序员能够像处理其他变量类型一样轻松处理动作或函数。
将函数用作函数的参数,或将函数作为值返回,这些概念属于高阶函数的领域。清单 4 对 清单 3 做了一点点修改,显示了能返回函数的高阶函数:
清单 4. 高阶函数
<head> <script type='text/javascript'> function temperature() { return current } hot = function hot() { alert('Hot.') } cold = function cold() { alert('Cold.') } current = hot function swap() { if(current == hot) { current = cold } else { current = hot } } </script> </head> <body> <button onclick="funct = temperature()();">Temperature</button> <button onclick="swap();">Swap</button> </body> |
这个例子解决了一个常见问题:如何将更改中的行为附加到用户接口事件?通过高阶函数,这很容易做到。temperature
高阶函数返回 current
的值,而 current 又可以有 hot
或 cold
函数。看一下这个有些陈旧的函数调用:temperature()()
。第一组括号用于调用 temperature
函数。第二组括号调用由 temperature
返回 的函数。图 4 显示了输出:
图 4. 高阶函数
高阶函数是函数式编程的基础,对比面向对象编程,函数式编程代表了更高级别的抽象。但 JavaScript 的实力并不仅限于高阶函数。JavaScript 的动态类型就极为适合 UI 开发。
|
|
动态类型
通过静态类型,编译器可以检查参数和变量的值或针对一个给定操作所允许的返回值。其优势是编译器可以做额外的错误检查。而且静态类型还可以为诸如 IDE 这样的工具提供更多信息,带来其他一些特性,比如更好的代码完成功能。但静态类型也存在着如下一些劣势:
- 必须提前声明意图,这常常会导致灵活性降低。例如,更改一个 Java 类就会更改类的类型,因而必须重新编译。对比之下,Ruby 允许开放的类,但更改一个 Java 类还是会更改类的类型。
- 要实现相同的功能,必须输入更多的代码。例如,必须用参数形式包括进类型信息,必须用函数形式返回值和所有变量的类型。另外,还必须声明所有变量并显式地转化类型。
- 静态语言的编译-部署周期要比动态语言的部署周期长,尽管一些工具可被用来在某种程度上缓解这一问题。
静态类型更适合用于构建中间件或操作系统的语言中。UI 开发常常需要更高的效率和灵活性,所以更适合采用动态类型。我深知这种做法存在危险。相信使用过 JavaScript 的 Web 开发人员都曾经为编译器本应检测到的错误类型的变量而绞尽脑汁。但它所带来的优势同样不可否认。下面将举例加以说明。
首先,考虑一个对象的情况。在清单 5 中,创建一个新对象,并访问一个不存在的属性,名为 color
:
清单 5. 引入一个属性
<script type='text/javascript'> blank_object = new Object(); blank_object.color = 'blue' alert('The color is ' + blank_object.color) </script> |
当加载并执行此应用程序时,会得到如图 5 所示的结果:
图 5. 引入属性
JavaScript 并不会报告 blue
属性不存在的错误。静态类型的拥护者大都会被本例所吓倒,因为本例中的错误被很好地隐匿了。虽然这种做法多少会让您感觉有些不正当,但您也不能否认它巨大的诱惑力。您可以很快引入属性。如果将本例和本文之前的例子结合起来,还可以引入行为。记住,变量可以保存函数!所以,基于动态类型和高阶函数,您可以在任何时候向类中引入任意的行为。
可以轻松地重写 清单 5,使其如清单 6 所示:
清单 6. 引入行为
<script type='text/javascript'> blank_object = new Object(); blank_object.color = function() { return 'blue'} alert('The color is ' + blank_object.color()) </script> |
从上例可以看出,在 JavaScript 的不同概念之间可以如此轻松地来回变换,其含义上的变化很大 —— 比如,是引入行为还是引入数据 —— 但语法上的变化却很小。该语言很好的延展性是它的一种优势,但同样也是其缺点所在。实际上,该语言本身的对象模型就是 JavaScript 延展程度的一种体现。
|
|
对象模型
到目前为止,您应该对 JavaScript 有一个正确的评价了,它绝非只如一个玩具那么简单。事实上,很多人都使用过其对象模型创建过极为复杂、设计良好的面向对象软件。但对象模型尤其是用于继承的对象模型又非您一贯认为的那样。
Java 语言是基于类的。当构建应用程序时,也同时构建了可以作为所有对象的模板的新类。然后调用 new
来实例化该模板,创建一个新对象。而在 JavaScript 中,所创建的是一个原型,此原型是一个实例,可以创建所有未来的对象。
现在先暂且放下这些抽象的概念,去查看一些实际代码。比如,清单 7 创建了一个简单的 Animal
,它具有 name
属性和 speak
动作。其他动物会从这个基础继承。
清单 7. 创建一个构造函数
<script type='text/javascript'> Animal = function() { this.name = "nobody" this.speak = function () { return "Who am I?" } } myAnimal = new Animal(); alert('The animal named ' + myAnimal.name + ' says ' + myAnimal.speak()); </script> |
清单 7 的结果如图 6 所示:
图 6. 创建一个构造函数
对于 Java 开发人员而言,清单 7 中的代码看起来多少有点生疏和奇怪。实际上对于没有亲自构建过对象的许多 JavaScript 开发人员来说,这些代码同样看起来有点生疏和奇怪。也许,下面的解释可以让大家能够更好地理解这段代码。
实际上,您只需重点关注其中三段信息。首先,JavaScript 用嵌套函数表示对象。这意味着清单 7 中的 Animal
的定义是一种有效的语法。第二,JavaScript 基于原型或现有的对象的实例来构造对象,而非基于类模板。funct()
是一种调用,但 new Animal()
却基于 Animal
内的原型构造一个对象。最后,在 JavaScript 中,对象只是函数和变量的集合。每个对象并不与类型相关,所以可以自由地修改这种结构。
回到 清单 7。如您所见,JavaScript 基于在 Animal
中指定的原型定义一个新对象:myAnimal
。继而可以使用原型中的属性和函数,甚或重定义函数和属性。这种灵活性可能会让 Java 开发人员受不了,因为他们不习惯这种行为,但它的确是一种十分强大的模型。
现在我还要更深入一步。您还可以使用名为 prototype
实例变量来指定对象的基础。方法是设置 prototype
实例变量使其指向继承链的父。如此设置 prototype
之后,您所创建的对象会为未指定的那些对象继承属性和函数。这样一来,您就可以模仿面向对象的继承概念。以清单 8 为例:
清单 8. 通过原型继承
<script type='text/javascript'> Animal = function() { this.name = "nobody" this.speak = function () { return "Who am I?" } } Dog = function() { this.speak = function() { return "Woof!" } } Dog.prototype = new Animal(); myAnimal = new Dog(); alert('The animal named ' + myAnimal.name + ' says ' + myAnimal.speak()); </script> |
在清单 8 中,创建了一个 Dog
原型。此原型基于 Animal
。Dog
重定义 speak()
方法但却不会对 name()
方法做任何改动。随后,将原型 Dog
设置成 Animal
。图 7 显示了其结果:
图 7. 通过原型继承
这也展示了 JavaScript 是如何解决到属性或方法的引用问题的:
- JavaScript 基于原始的原型创建实例,该原型在构造函数中定义。任何对方法或属性的引用都会使用所生成的原始副本。
- 您可以在对象内像定义其他任何变量一样重新定义这些变量。这样做必然会更改此对象。所以您显式定义的任何属性或函数都将比在原始的原型中定义的那些属性或函数优先级要高。
- 如果您显式设置了名为
prototype
的实例变量,JavaScript 就会在此实例中寻找任何未定义的实例变量或属性。这种查找是递归的:如果 在prototype
内定义的实例不能找到属性或函数,它就会在其 原型中查找,依此类推。
那么,JavaScript 的继承模型到底是什么样的?这取决于您如何对它进行定义。您需要定义继承行为以便可以覆盖它。然而,从本质上讲,JavaScript 更像是一种函数式语言,而非面向对象的语言,它使用一些智能的语法和语义来仿真高度复杂的行为。其对象模型极为灵活、开放和强大,具有全部的反射性。有些人可能会说它太过灵活。而我的忠告则是,按具体作业的需要选择合适的工具。
|
|
结束语
JavaScript 对象模型构建在该语言的其他功能之上来支持大量的库,比如 Dojo(参见 参考资料)。这种灵活性让每个框架能够以一种精细的方式更改对象模型。在某种程度上,这种灵活性是一种极大的缺点。它可以导致可怕的互操作性问题(尽管该语言的灵活性可以部分缓解这些问题)。
而另一方面,灵活性又是一种巨大的优势。Java 语言一直苦于无法充分增强其灵活性,原因是它的基本对象模型还未灵活到可以被扩展的程度。一个典型的企业级开发人员为能够成功使用 Java 语言必须要学习很多东西,而新出现的一些优秀的开放源码项目和新技术,比如面向方面编程、Spring 编程框架和字节码增强库,则带来了大量要学的代码。
最后,JavaScript 优秀的灵活性的确让您体会到了一些高阶语言的强大功能。当然您无需选择为每个项目或大多数项目都做这样的权衡和折衷。但了解一种语言的优势和劣势 —— 通过参考大量信息,而不仅仅基于广告宣传或公众意见 —— 会让您可以更好地控制何时需要使用以及何时不能使用这种语言。当您在修改 JavaScript Web 小部件时,您至少知道该如何让此语言发挥它最大的优势。请继续跨越边界吧。
参考资料
学习- 您可以参阅本文在 developerWorks 全球站点上的 英文原文 。
- Java To Ruby: Things Every Manager Should Know (Pragmatic Bookshelf,2006 年):本文作者所著的书,介绍了何时何地可以从 Java 编程转换到 Ruby on Rails,以及如何进行转换。
- Beyond Java (O'Reilly,2005 年):本文作者所著的书,介绍了 Java 的兴起和兴盛,以及能够在某些领域挑战 Java 平台的技术。
- Scriptaculous 和 Prototype:两种 JavaScript 框架,通过 Ruby on Rails 助力 Ajax。
- Object Hierarchy and Inheritance in JavaScript :有关 JavaScript 继承的很好资源。
- Ajaxian :这个 Ajax 的门户网站上有所有有关 Ajax 的杰出讨论,极大地推动了 JavaScript 的复苏。
- Dojo :一个 JavaScript 开放源码工具箱。
- Rhino :JVM 上的 JavaScript 引擎。
- 用函数式编程技术编写优美的 JavaScript (developerWorks,2006 年 7 月):作者解释了如何使用 JavaScript(TM)™(JavaScript 能导入函数式编程的构造和特性)编写优美的代码。
- Java 技术专区 :数百篇 Java 编程各方面的文章。
- Ajax 技术资源中心 :获得 developerWorks 上有关 Ajax 的所有信息。