JavaScript = C + Lisp

作者: William Taysom

原文地址:http://www.jadetower.org/muses/archives/000307.html

翻译:ShiningRay

我在过去的几周内一直在写JavaScript代码——使用我们的对话框系统来个性化Mozilla。假设你要求:“嘿,电脑,我要教你如何在Amazon.com上找书。首先你象这样进入Amazon,然后在这里输入你要的书的名字。点击“Go”然后……”我的困难在于对Mozilla编码使我的对话框系统可以“看”浏览器中正在进行什么然后自己可以执行这些动作。

由于Mozilla中较高的层次是用JavaScript实现的。所以我一直在废寝忘食研究它(我的Rhino book里面全是我做的书签)我写的越多,我越觉得它像Lisp。

考虑以下代码:

这里SemTree是一个对象,它允许你从一个HTML DOM 树中选出某些你感兴趣的节点,去掉那些你不感兴趣的节点。(根本上说,这是一个TreeWalker 类的包装器。)若要建立一个 SemTree ,你要给出一个接受器。一个接受器只是一个判断给定节点是否能被接受的一个函数:

一旦有了一些基本的接受器和筛选器,很容易就可以定义组合筛选器──一种将筛选器以特殊形式组合起来的函数:

当它被调用的时候,以接受器作为参数,acceptAny返回一个新的接受器,可以接受只要是 disjuncts 能接受的那些给定节点 n。所以,semanticAccepter 中出现的acceptAny 能接受文本、链接、表单和链接中的图像。相反地,acceptOnlyIf只能接受被所有接受器组件接受的节点。acceptOnlyIf的定义类似于acceptAny

acceptOnlyIfacceptAny如此相似让我纳闷是否有一个通用的方法可以将任一个组合器(像否定、短路与、短路或)变成一个组合筛选器(象acceptNotacceptOnlyIfacceptAny)。的确有,但JavaScript不胜任这个任务。要完成这个,我们需要更强大的武器。

将函数的能力定义得和单子的功能一样微小

通过提供第一类型函数,JavaScript迈出了进入更广阔世界的第一步。而这个世界中最具影响的便是Haskell和它的一些变体。由于接受器告诉我们是否要接受一个节点,它们应该是一个从节点到布尔值的函数。在Haskell中,我们这样写:

这样,否定是一个布尔到另一布尔,合取和析取是从一个布尔的列表到一个单个布尔值,这样写:

组合接受器有类似的形式:

我们给semanticAccepter下的定义和JavaScript版的类似:

我们怎样定义一个类似acceptOnlyIf的组合器?Haskell没有像一些语言中的命令结构。取代的是递归:

由于某些原因,短变量名是Haskell中的标准。n是一个节点,a是一个接受器,as是一个接受器的列表。(以’s’结尾是代表某个变量是列表的标准形式。)你上面看到的定义是正确的,但我不常用这个方法。我会使用一个优美的小函数叫map

当传了一个函数和一个列表,map返回对列表中的每个元素执行函数后生成的列表。有了map后,acceptOnlyIf
就可以这样定义。

这里语法 (\a -> a n) 基本意思和JavaScript下面的一样:

整个acceptOnlyIf的定义本质上说明了,“给出一个节点n,找出每个接受器对n的值,然后返回这些值的合取值(和值AND)。”有了这种定义函数的优美方法之后,它们之间的相似之处立刻显现出来了:

这样,泛化就是一些琐碎的事了:

现在我们是否可以更进一步了呢?我们是否可以也将 not搞成 acceptNot呢?andnot的主要区别在于 and参数是一个布尔值的列表,而not只能针对单个布尔值。要更进一步泛化 liftCombiner,我们必须:

  1. 找出可以描绘出基本值和列表的共同特性的结构。
  2. 将这个结构应用到合成组合器的问题中.

Haskell 正好有我们所需要的。它称为单子Monad.

什么是单子?

之前就有人问过这个问题。单子到处可见。大多数结构和过程/进程像数据类型、函数、对象、、异常、I/O、副作用、同步、事务、分析器和编程语言,都可以接受单独的、原子的操作。组对(Pair)、元组(tuple)、列表、树、图——这些数据结构都有一个单子级的解释,是常常不止一个。由于是单子的东西太多了,所以很难对它们进行描述。不过我还是可以给你一个数学上的定义。但是正如“A continuation is the rest of the computation ”所说的,给出单子的定义只有在你已经对它有了一些感性的认识才有用。否则直接给出定义只会混淆你的观点。那先让我们研究一下这个谦虚的列表作为我们受到单子启发的途径。

检验结构有很多方法。其中一个方法是查看各部分是如何一起工作来组成整体的。当以这种方法分析列表的时候,我们发现它们是连接的解码:一个列表要么是空的要么是一个由一个head和一个tail组成的Cons(一种构造函数,返回一个新的列表)。head是一个列表的元素,tail则是列表的其它部分。

若要定义列表[1, 2, 3],我们这样写:To define the list [1, 2, 3], we write:

Haskell中的列表就是这样工作的。除了用[]代表 Nil和用: 代表 Cons。逗号可以用来分隔条目,可以写成这样:

链表有一个很长很有名的历史。不幸的是分解材料并不会显露出单子。

另一种分析列表的方法是通过研究它是如何和其它东西相关、进行交互的。Haskell提供了“class”机制根据和它们相关联的方法来定义对象。类似于抽象数据类型和接口:

这是一个看似完美的列表类,但它几乎没有从分解材料中抽象出什么东西。所有的方法,除了 map,都是特定于列表的逻辑结构的,map抓住了一个较抽象的概念。它将一个函数作为参数,然后返回一个被函数处理过新的列表。回忆一下 map 对定义 acceptAny acceptOnlyIf 起了多大的帮助?这很明确是个值得研究的函数。

还有哪些其它函数对列表作为一个独立于它特定实现和形态的数据结构来说是至关重要的呢?好吧,还应该有一个方法 unit来把一个单独的元素放入一个列表,而且我们还需要一个 join 来将一个列表的列表组成一个长的列表。这个类定义像这样:

以下是链表的实现:

这些方法一目了然:unit将参数放入列表,map对列表中的每个元素执行函数,join将一个列表的列表连成一串。让我们确定一下串连接符(++)的定义吧:

这些函数都十分简单而且十分有用。它们确实是列表的标准成分,但就好比火药一样,如果你把它们正确地组合起来,你就能引爆一些东西:一个单子。我们等不及这个式子了:

就是它了。两个函数:return将一些东西放入单子中,同时(>>=),称为“绑定”,将一个单子变成另一个。我们立即来看看单子到底能做什么。不过首先为了明确起见,我们先将我们的列表实现扩展成应用一个单子:

return定义和 unit一样:将它自己放入一个列表中。绑定 (>>=)将mapjoin 组合成一个操作。首先你将函数k映射到列表 ls ,然后由于 f返回一个列表的列表,你再用 join 将结果组成单个列表。

但单子这东西有什么好处呢?设想你想要将一个列表中的所有值和另一个列表中的值相加来产生一个大的列表:

JavaScript代码应该像:

应用了单子我们可以这样写:

代码看来很简练,但没什么特别的。幸运的是,由于单子是如此有用,所以更好的语法形式也出现很久了。我们应该这样写:

这称为“do notation做标记”为了明显起见。还有另一种变体,我个人喜欢叫“列表包含语法”或者“我无法相信这没有成为一个学说”:

不论哪种方法,都是所有的单子之上的一种较好语法。这种较好语法也许看上去像某种命令语言或什么蛇的东西(指Python语言──译注)。但我不接受其它替代语法,在Haskell中任何单子都能使用两者之一。

我们看看一个列表单子是如何工作的,但我们仅有的单子是 LinkedList。对于not,我们只需要一个直接变换值的单子就行了:一个一致单子。它不会对值做特殊的改变,仅仅直接返回值:

现在我们终于可以完整地定义 liftCombiner了:

现在“让组合器能工作在所有接受器上”的这个想法已经不难实现了:

最后的思考

今天我们看了如何处理组合的接受器(从节点到布尔的函数),组合器可以是任何单子。结果接受器也是单子。你认为是否有一种方法,可以让组合器组合任意单子和其它单子(除了接受器之外)?如果有,怎样做?如果没有,单子之间要有怎样的关联才能这样?

在Java2平台企业版中应用异步JavaScript技术和XML(AJAX)

作者Greg Murray, 2005年6月9日  翻译:ShiningRay@Nirvana Studio2005年9月9日
任何试过过Flickr、GMail、Google Suggest或者是Google Maps的人都会意识到一种新型的动态Web应用正在逐渐浮出水面。这些应用外观和表现都和传统的桌面应用程序很像,而他们不需要依赖于插件或者是特定于浏览器的功能。过去Web应用只是一系列HTML页面,他们任意一部份内容的更改都必须重新载入页面。像JavaScript编程语言和层叠样式表 (CSS)之类的技术已经成熟,可以有效地应用他们来创建高动态的Web应用,而且可以运行在所有的主流浏览器中。本文将会详细介绍你马上就可以使用的一些技术,让他们使你的Web应用像桌面应用更加丰富和更有交互性。

介绍异步JavaScript技术和XML(AJAX)


使用JavaScript技术,一个HTML页面可以异步地对服务器(一般是载入页面的服务器)发送请求并获取XML文档。然后JavaScript可以使用XML文档来更新或改动HTML页面的文档对象模型(DOM)。最近形成了一个术语AJAX(Asynchronous JavaScript Technology and XML)来描述这种交互模型。

AJAX其实不是很新的东西。这些技术对于Windows平台上专注于Internet Explorer的开发人员来说,已经存在好几年了。直到最近,这个技术才被作为Web远程技术或者远程脚本技术被大家了解。Web开发人员也有一段时间曾经使用过插件、Java applet和隐藏框架来模拟这种交互模型。最近发生的变化是,对XMLHttpRequest对象的支持已经成为所有平台上的主流浏览器都包括的特性了。JavaScript技术的XMLHttpRequest对象是。尽管在正式的JavaScript技术标准中并没有提到这种对象,然而今天主流的浏览器都对他提供了支持。而当代的浏览器如Firefox、Internet Explorer以及Safari在JavaScript技术和CSS的支持上有些细微的差别,但是这种差别是可以处理的。如果你要考虑支持较老的浏览器,AJAX也许就不能成为你的解决方法。

基于AJAX的客户端之所以独特的原因是客户端包含了用JavaScript嵌入的特定于页面的控制逻辑。应用JavaScript技术的页面基于事件进行交互,如文档载入、鼠标点击、焦点改变甚至是定时器。AJAX交互使得表现层逻辑更加清晰地与数据分离。一个HTML页面也可以根据需要每次读入适当的数据,而不是每次需要显示一个更改时都重新载入整个页面。AJAX要求一种不同的服务器架构来支持它这种交互模型。以前,服务器端Web应用关注于对每个导致服务器调用的客户端事件都生成HTML文档。然后客户端对每个回应都要重新读入并重新渲染完整的HTML页面。富Web应用(Rich Web Application)关注于,让一个客户端获取一个HTML文档让它表现为一个模板或者是一个容器,可以基于事件并使用从服务器端组件中获取的XML 数据来对文档注入内容。

一些AJAX交互的应用如:

  • 实时表单数据检验:像用户ID、序列号、邮政编码或者是特殊的票据代码这类需要服务器端验证的数据也可以在用户提交表单之前进行验证。
  • 自动补全:像电子邮件地址、姓名或城市名之类的表单数据都可以根据用户情况自动补全。
  • 处理细节操作:根据一个客户端事件,一个HTML页面可以根据现存的一些数据再去获取更多详细的信息,如现在有一个产品列表,客户端可以控制查看单独的产品信息而无需刷新页面。
  • 复杂的用户界面控件:像树型控件、菜单和进度条之类不要求页面刷新的控件也能实现。
  • 页面内刷新数据:HTML页面可以从服务器上查询最新的数据如分数、股指、天气还有其它的特定于应用的数据。
  • 服务器端通知:一个HTML页面可以通过对服务器进行定时查询来模拟一个服务器的事件通知推送,实现像通知客户端一个消息、刷新页面数据或将客户端重定向到另一个页面。

这个列表并未把所有的应用都列出来,但它已经显示了AJAX交互可以让Web应用比从前能做更多的事情。但尽管这些好处是值得关注的,这种方式也有一些缺点:

  • 复杂度:服务器端开发人员必需理解,HTML客户端页面中的表现层逻辑以及生成HTML客户端页面所需的XML内容的服务器端逻辑。HTML页面开发人员必须了解JavaScript技术。如果开发新的框架和发展已有的框架来支持这种交互模型,那么AJAX应用的创建就会越来越简单。
  • XMLHttpRequest对象的标准化:XMLHttpRequest对象还不是JavaScript技术标准的一部分,这就意味着根据客户端的不同,应用的行为也有所会不同。
  • JavaScript技术的实现:AJAX交互极大地依赖于JavaScript技术,而由于客户端的原因JavaScript还有一些细微的差别。见QuirksMode.org来了解更多关于浏览器之间区别的内容。
  • 调试:AJAX应用也难于调试,因为流程逻辑是同时嵌在客户端中和服务器上的。
  • 代码可见:客户端的JavaScript可以很容易通过“查看源代码”被人看见。一个没有良好设计的AJAX应用很可能被黑客攻击或被他人剽窃。

当开发人员在使用AJAX交互模型上获得更多的经验后,AJAX技术的框架和模式就会慢慢浮现出来。现在就关注于完全通用的AJAX交互框架,还为时过早。本文和相关的解决方案将关注于在现有的Java 2平台企业版(J2EE)上如何对AJAX进行支持,像servlet,JavaServer Page(JSP)软件、JavaServer Face应用和Java标准标签库(JSTL)。

AJAX交互剖析


现在我们已经讨论了AJAX是什么以及一些高层次的问题。那现在让我们把所有的零件放在一起来展示一个具有AJAX的J2EE应用。

首先考虑一个例子。一个Web应用包括了一个静态HTML页面,或者是一个由JSP生成的HTML页面,这个JSP中还包括了一个HTML表单,它需要服务器端逻辑来对表单中的数据进行检验,而不用刷新页面。一个名为ValidateServlet服务器端组件(servlet)用来提供这种验证逻辑。图一描述了这种具有验证逻辑的AJAX交互的细节。

AJAX Interaction
图1: 一个提供验证逻辑的AJAX交互

以下条目代表了图1中出来AJAX交互的过程:

  1. 发生一个客户端事件
  2. 创建和配置一个XMLHttpRequest对象。
  3. XMLHttpRequest对象进行一个调用。
  4. ValidateServlet对请求进行处理。
  5. ValidateServlet返回一个包含了结果的XML文档。
  6. XMLHttpRequest对象调用callback()函数并处理结果。
  7. 更新 HTML DOM。

现在让我们逐个研究这个AJAX模型的每一步。

1.发生一个客户端事件。

在一个事件发生时可以调用相应的JavaScript函数。在这里,validate()函数可以被映射到一个链接或者是表单组件的onkeyup事件上去。

<input type="text"
size="20"
id="userid"
name="id"
onkeyup="validate();">

每次用户在表单域中按下一个键时,表单元素将都调用validate()函数。 

2. 建立和配置一个XMLHttpRequest对象

创建和配置一个XMLHttpRequest对象

var req;

function validate() {
var idField = document.getElementById("idField");
var url = "validate?id=" + escape(idField.value);
if (window.XMLHttpRequest) {
req = new XMLHttpRequest();
} else if (window.ActiveXObject) {
req = new ActiveXObject("Microsoft.XMLHTTP");
}
req.open("GET", url, true);
req.onreadystatechange = callback;
req.send(null);
}

validate()函数建立了一个XMLHttpRequest对象并对象中的open函数。open函数需要两个参数:HTTP方法,可以是GETPOST; 和对象进行交互的服务器端组件的URL;一个布尔变量,表示是否要进行异步调用。API是XMLHttpRequest.open(String method, String URL, boolean asynchronous)。如果一个交互被设置为异步, (true) 那就必须指明一个回调函数。可以使用req.onreadystatechange = callback;来设置这个交互的回调函数。详细内容见第六节。

3.XMLHttpRequest对象进行调用

当收到了语句req.send(null);,就会进行一次调用。HTTPGET的情况下,内容可以是null或者留空。当调用XMLHttpRequest的这个函数时,也会对已经配置了的URL进行调用。在下面这个例子中,要发送的数据(id)将作为一个URL参数。

使用HTTPGET,两个重复的请求将返回同样的结果。当使用HTTPGET方法时,要注意URL的长度,包括已经转义的URL参数,可能会受到某些浏览器和服务器端的Web容器的限制。当发送的数据会影响到服务器端的应用程序的状态时,就应该使用HTTPPOST方法。使用HTTPPOST必须要对XMLHttpRequest对象设置一个Content-Type头,使用以下语句:

req.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
req.send("id=" + escape(idTextField.value));

当从JavaScript中发送表单值得时候,你应该考虑对字段值进行编码。JavaScript中有一个函数escape(),应该用他来确保区域化的内容被正确编码,同时特殊字符也被正确转义。

4. ValidateServlet对请求进行处理.

一个映射到URI “validate” 的servlet将检验user ID是不是已经在数据库中存在了。

一个servlet处理一个XMLHttpRequest ,就像对待其它的HTTP请求一样。下面的例子显示了服务器从请求中抽取出id参数并检验是否被占用了。

public class ValidateServlet extends HttpServlet {

private ServletContext context;
private HashMap users = new HashMap();

public void init(ServletConfig config) throws ServletException {
this.context = config.getServletContext();
users.put("greg","account data");
users.put("duke","account data");
}

public void doGet(HttpServletRequest request, HttpServletResponse response)
throws IOException, ServletException {

String targetId = request.getParameter("id");

if ((targetId != null) && !users.containsKey(targetId.trim())) {
response.setContentType("text/xml");
response.setHeader("Cache-Control", "no-cache");
response.getWriter().write("<message>valid</message>");
} else {
response.setContentType("text/xml");
response.setHeader("Cache-Control", "no-cache");
response.getWriter().write("<message>invalid</message>");
}
}
}

在这个例子中,一个简单的HashMap用来存放存在的用户名。在这个例子中,我们假设用户的ID是duke

5.ValidateServlet返回一个包含结果的XML文档

用户ID “duke” 在users HashMap的用户ID列表中出现了。将在应答中写一个包含值为invalidmessage元素的XML文档。更复杂的用例将要求DOM、XSLT或其他API来生成这个应答。

response.setContentType("text/xml");
response.setHeader("Cache-Control", "no-cache");
response.getWriter().write("<message>invalid</message>");

开发人员必须注意两个事情。第一,Content-Type必须设为text/xml。第二,Cache-Control必须设为no-cacheXMLHttpRequest对象只会处理Content-Typetext/xml的应答,同时把将Cache-Control设为no-cache将确保浏览器不会从缓存相同的URL(包括参数)返回的应答。

6.XMLHttpRequest对象调用callback()函数并处理结果。

XMLHttpRequest对象已经配置为当有readyState改变的时候就调用callback()函数。让我们假设已经ValidateServlet调用了而且ValidateServlet4,表示XMLHttpRequest的调用已经完成。HTTP状态代码200表示一个成功的HTTP交互。

function callback() {
if (req.readyState == 4) {
if (req.status == 200) {
// update the HTML DOM based on whether or not message is valid
}
}
}

浏览器维护了一个所显示的文档的对象形式(也就是所谓的Docuemt Object Model或DOM)。HTML页面中的JavaScript可以访问DOM,同时在页面载入完之后,可以使用API来修改DOM。

根据成功的请求,JavaScript代码可以修改HTML页面的DOM。从ValidateServlet获得的对象形式的XML文档可以通过req.responseXML在JavaScript中获得,req是一个XMLHttpRequest对象。DOM API给JavaScript提供了获取这个文档中的内容以及修改HTML页面的DOM的方法。所返回的字符串形式的XML文档可以通过req.responseText获得。现在我们看看如何在JavaScript中使用DOM API,先看以下从ValidateServlet返回的XML文档。

<message>
valid
</message>

这个例子是一个简单的只包含了一个message元素的XML片断,里面只有一个简单的字符串validinvalid。一个更高级的例子可以包含多于一个的消息和可以给用户看的有效的名字:

function parseMessage() {
var message = req.responseXML.getElementsByTagName("message")[0];
setMessage(message.childNodes[0].nodeValue);
}

parseMessages()函数将处理一个从ValidateServlet获取的XML文档。这个函数会调用setMessage()with the,并给出message作为参数来更新HTML DOM。

7.更新HTML DOM

JavaScript技术可以使用很多API从HTML DOM中获得任何元素对象的引用。推荐的获得元素引用的方法是调用document.getElementById("userIdMessage")"userIdMessage"是HTML文档中出现的一个元素的ID属性。有了这个元素的引用,就可以使用JavaScript来修改元素的属性、修改元素的样式、添加、删除或修改子元素。

一个常见的改变元素主体内容的方法是设置元素的innerHTML属性,如下所示:

<script type="text/javascript">
function setMessage(message) {
var userMessageElement = document.getElementById("userIdMessage");
userMessageElement.innerHTML = "<font color=\"red\">" + message + " </font>";
}
</script>
<body>
<div id="userIdMessage"></div>
</body>

受到影响的那部分HTML页面会立刻根据innerHTML的设置重新渲染。如果innerHTML属性包含类似<image>或者是<iframe>之类的元素,那么由那些元素所指定的内容同样会被获取并渲染。

这种途径的主要缺点是HTML元素是作为字符串硬编码在JavaScript中的。JavaScript中硬编码的HTML标记不是一种好的实践,因为它使代码难于阅读、维护和修改。我们应该考虑在JavaScript中使用DOM API来创建和修改HTML元素。把显示和JavaScript代码的字符串混在一起只会让页面更难于阅读和编辑。

另一种修改HTML DOM的方法是动态地产生新的元素并把他们作为子元素追加到目标元素,如下面的例子所示:

<script type="text/javascript">
function setMessage(message) {
var userMessageElement = document.getElementById("userIdMessage");
var userIdMessageFont = document.getElementById("userIdMessageFont");
var messageElement = document.createTextNode(message);

if (userMessageElement.childNodes[0]) {
// 更新元素
userIdMessageFont.replaceChild(messageElement, userIdMessageFont.childNodes[0]);
} else {
// 建立一个新的元素
var fontElement = document.createTextNode("font");
fontElement.setAtribute("id", "userIdMessageFont");
fontElement.setAtribute("color", "red");
userMessageElement.appendChild(fontElement);
fontElement.appendChild(messageElement);
}
}
</script>
<body>
<div id="userIdMessage"></div>
</body>

这个范例展示了JavaScript技术的DOM API可以用来更有目的地建立或改变一个元素。当然JavaScript的DOM AP在不同的浏览器上也可能有差别,所以你必须在开发应用程序时小心。

Java BluePrint的解决方案目录

TheJava Blueprints Solutions Catalog是用来收集J2EE技术上AJAX的最佳实践的。每个解决方案包含一个问题和方法的描述、一个设计文档和可运行的源码。这些解决方案是为了让你根据需要在自己的应用程序中复用。以下是已经提供的AJAX交互:

自动补全

自动补全提供了当用户在一个HTML表单中输入一个请求时对数据浏览的简化方式。当用户面对一大片数据时,可以在输入数据时把可能的完整形式显示给用户。然后选择其中一个完整形式可以保证用户输入的数据已经存在在服务器上。

考虑一个大公司的一个名字查找的Web应用。如图2所示,只要输入姓或名的开头几个字母就可以得到人的列表。用户可以然后就只要点击一下就可以浏览用户的详细信息。

Autocompletion of a Name
图2:名字自动补全

进度条

在Web应用中,一个服务器端任务也可能要花一段时间去完成。这段时间很可能会超过HTTP交互的时间上限(超时)。当用户不知道这个任务什么时候才能完成时,用户很可能会重新提交一次表单或直接退出会话状态。一般来说,Web应用使用页面刷新来跟踪服务器端操作的状态,这种方式可能会让人厌烦而且也不准确。AJAX可以用来仅在一个HTML页面中跟踪服务器端操作的状态而无需刷新页面。用户可以以图形方式看到服务器端操作的进度,如图3。

Progress Bar
图3:进度条 

刷新数据

向一个HTML页面提供最新的数据或服务器消息提醒在现在的Web世界中也是十分重要的,因为现在的Web世界中数据一直不停变化。尽管它不是一个实实在在的推送技术,但它可以通过使用AJAX交互不断进行查询来模拟。当数据需要更新或者要进行提醒,HTML页面将会动态地改变。图4显示了HTML页面中的一个服务器端计数器。这个计数器会在页面后台自动更新。

Server-side Counter Shows Refreshing Data
图4:服务器端计数器在刷新数据

实时检验

不是所有的表单域都可以单独用JavaScript技术在客户端完成。某些表单数据要求服务器端的验证逻辑。传统和Web应用曾使用页面刷新来完成这种验证,但这可能有些让人烦。

考虑一个需要一个唯一用户ID的Web应用。使用AJAX交互,用户可以在输入的时候就知道ID是否有效(图5)。

Invalidating the ID as User Types
图5:指出用户ID无效

当一个用户输入了一个无效的用户ID,应用程序禁止了提交按钮并且向用户显示了一个信息(图6)。

Validating the ID as User Types
图6:用户ID通过验证

用户马上就能知道用户ID是可用的也是有效的。

最后的思考

我们已经看到AJAX交互可以解决很多问题。配合HTTP处理、数据库、Web服务、XML处理和业务对象等API,J2EE技术已经提供了一个开发和部属基于AJAX应用的一个良好的基础。有了对于这个交互模型的更好的理解,今天的应用程序可以变得更加有交互性,给最终用户更好的体验。

使用AJAX要求你使用支持XMLHttpRequest对象的最新浏览器版本。使用AJAX还要求大量对JavaScript技术和CSS的应用。作为一个应用程序架构师或是一个开发人员,你要会针对浏览器支持、架构复杂度和对开发人员的培训等方面来衡量开发一个富应用的需要。当AJAX编程模型不断地发展,现有的技术和框架会让这种转变更加容易。

很明显的是,突出的Web应用都越来越有交互性了。那么你的呢?

更多信息

关于作者


Greg Murray 是is a Sun Microsystems 的一名工程师,是servlet标准的领导人,BluePrint小组的前成员,在这个小组时他已经开始关注Web层次问题。他也是《Enterprise Applications With the Java 2 Platform,Enterprise EditionDesigning Web Services With the J2EE 1.4 Platform(Addison-Wesley)》一书的协助编撰者。

REBOL 语言简介

翻译:ShiningRay @ NirvanaStudio

本文是一篇针对新用户的技术文档。他给出了一个REBOL的简明概述和一个例子。

Quick Facts

  • REBOL 代表基于相关表达式的对象语言(Relative Expression Based Object Language).
  • REBOL 发音为"reb-ol" ,和“rebel with a cause”中的一样。
  • REBOL 是一种消息式的语言。他的主要目的是提供一种轻量级的分步计算和通讯的方法。
  • REBOL Carl Sassenrath设计, 是Amiga OS——世界上第一个个人电脑的多任务操作系统——的系统架构师。
  • REBOL 不仅仅只是一个编程语言。他还是一种用于表示数据和元数据的语言。他给计算、存储和信息交换提供了同一种方法。
  • REBOL 有十分丰富的软件包。初学者可以从REBOL/Core和REBOL/View开始。专业人士会发现REBOL/Command和REBOL/SDK十分有用。公司和组织会发现REBOL/IOS是一个强大的协作交流平台。
  • REBOL 代码和数据跨越了40多种平台。在Windows上写的脚本和在Linux, UNIX和其他平台上运行得都一样,根本无须任何改动。
  • REBOL 引入了方言化的概念。方言是一个小巧有效专门针对特殊领域的子语言。
  • REBOL 本意就是要保持小巧——即使他已经包含了数百条函数,几十种数据类型,内置帮助,多种Internet协议、压缩、错误处理、调试控制台、加密和更多。
  • REBOL 程序很容易书写。你所需要的仅仅是一个文本编辑器。一个程序可以只有一行也可以是一整个应用程序包含了几十个文件。。同时,你的程序不需要对库和包含做声明。

上手例子

在REBOL中,简单的任务有简单的做法。这里有一些例子:

如果你想尝试这些例子,只要下载REBOL。他确实很小,下载得很快。它不需要任何复杂的安装也不需要任何特殊的类库。他很简单。要运行REBOL,只要点击他的图标。(如果你在使用REBOL/View,在他启动之后点击console图标来打开控制台窗口。)

先尝试一下经典的Hello World例子:

或者,把它作为一个邮件消息发送给一个朋友:

要直接从一个网站运行一个脚本

这个叫本会显示你的计算机有多快。

如果要查看脚本的源代码:

如果要保存源代码到一个文件:

要在本地运行代码,你可以输入:

当然,你可以用同样的方法保存任何网页:

要给一个朋友通过邮件发送一个网页:

要下载一封REBOL的拷贝,你要指明它是二进制的。像这样:

同时,你可以看见REBOL并不关心你代码中的换行。

如果你要把一个目录中的所有文件作为邮件发送(假设他们都是文本文件):

但是,也许你只想发送包含单词REBOL的文件:

如果你想把那些文件连成一个单独的消息并发送出去:

进阶例子

如果要查看你所有的email消息但是不把他们从服务器上删除,这样:

当然,你也许想一次性都读出他们:

要删掉所有包含单词“spam”的邮件:

当你准备发送一个信息到你的经纪人,信息可以被她的基于REBOL的股票交易程序自动处理:

以上的例子用到了REBOL的dialecting(方言化)概念——一种在网络上收发消息的极其有用的技术。

如果你的老板要求你每小时自动发送几个网页作为邮件:

如果你需要摘录并打印网页的标题,你可以这样解析:

生成脚本文件

你可以把上述任何例子输入文本编辑器(如Windows上的Notepad)然后保存为一个文件。你可以把这个文件拖至REBOL图标的上方来启动它。

你需要添加一个简短的REBOL头到你的文件的顶端:

这个标头表示这个文件是一个REBOL的代码同时可以提供一些有用的文档说明和其他的选项。

你的程序应该看起来像这样:

如果你正在使用REBOL/View,你也许想在你的脚本的底部添加一行这样你可以看到结果(在窗口关闭之前)。只要在结尾添加一个halt,这样控制台窗口就会开着:

内嵌帮助

如果要在REBOL中获得帮助,输入:

你可以查询某个特殊字的信息:

或者,查找符合一个字符串模式的帮助:

或者,察看REBOL所有的数据类型:

然后你可以查看REBOL中所有的高级函数用:

当然,还有本地代码、动作和其他类型的函数。

如果你想看高级函数的代码:

通过查看内嵌函数的代码,你可以了解很多有用的REBOL的编程方法。

仅仅是一个开始

我们希望以上的例子能给你一个好的REBOL的概念。我们在保持REBOL简洁上作了相当多的工作。当然,REBOL远不止这些东西。在,REBOL是一个高级编程语言,它在很多地方都很独特。常常有人说REBOL就像一片湖水,你看到了表面,同时你可以在湖面上尽情欢乐,但这不是湖水的全部。仍然有很多东西需要去深入了解。当然,作为初学者你不需要去了解那些东西。当你学到更多的东西,我们希望你能发现REBOL到底能做多少事情。请看一下我们的文档索引如果你想“游”得更深一些。

REBOL十步

翻译:ShiningRay @ NirvanaStudio

这是REBOL的一个简短概要。他在十步之内展示了REBOL的基本概念,仅仅需要阅读几分钟而已。更多的详细信息可以在 REBOL/Core Manual中找到。

1. 值(Value)

是REBOL的未处理的数据。不同的值可以在REBOL中直接书写。你已经对他们很熟悉了。这里有一些例子:

数字:
1234
-432 3.1415 1.23E12
0,01 1,2E12

时间:

12:34 20:05:32 0:25.34

日期:

20-Apr-1998 20/Apr/1998 (US)
20-4-1998 1998-4-20 (国际通用)

1980-4-20/12:32 (有时间的日期)
1998-3-20/8:32-8:00
(带着一个时区)

货币:

.34 USD.34 CAD3.45 DEM34,56

元组:

3.1.5 (版本号)
255.255.0 (颜色值)
199.4.80.7
(IP地址)

字符串:

"Here is a string"
{Here is another way to write
a string that
spans many lines
and contains "quoted"
strings.}

标签:

<title> </body>
<font size="2"
color="blue">

Email:

info@rebol.com

pres-bill@oval.whitehouse.gov

URLs:

http://www.rebol.com (网页)
ftp://ftp.rebol.com/sendmail.r (文件传输协议)

mailto:info@rebol.com (email)

文件:

%data.txt
%images/photo.jpg

%../scripts/*.r

号码:
#707-467-8000
(电话号码)
#0000-1234-5678-9999 (信用卡)
#MFG-932-741-A
(模型数字)

二进制:

#{0A6B14728C4DBEF5} (十六进制编码)
64#{45kLKJOIU8439LKJklj}
(base-64
编码)

只要可能,REBOL允许国际通用格式的使用,像十进制数、货币和日期。更多关于值得信息可以在 REBOL/Core Manual 中找到。

2. 词(Word)

词Words
是REBOL的符号。他们用来直接表示某个东西为一个词,或者间接表示为一个变量(稍后会解释)。你写词就和写英文一样的(或者和其他一些自然语言一样)。

破折号和一些其他的特殊字符也可以使用。他们会被认为是词的一部分。

单词的末尾被标记为一个空格,或者以下某个字符:

同样,这些字符不能出现在单词中:

更多关于单词的内容可以在 REBOL/Core Manual.中找到

3. 块(Block)

值和词都是在块中组合。一个块由一个'[‘开始,以’]’结束。你可以以任何顺序组织值和词,用任意多行代码。REBOL允许在块中以一种自由体来创建数据库、目录、表格、集合、序列、代码、函数,等等。

块通常用来组织代码和数据:

关于块的更多信息可以查阅 REBOL/Core Manual.

 

4. 计算(Evaluation)

块可以被计算得到他们的值。块中的值和词都按照从左到右的顺序计算。下面的例子展示了他如何工作的,假设你是输到之REBOL的命令行。Blocks
can be evaluated to compute their results. The values and words of
the block are computed from left to right. The examples below show how this
works as if you typed into REBOL at the prompt. (Lines typed at the prompt
create a block, even though the brackets [ ] are not shown):.

当计算块时,它的结果以一个值返回:

括号可以用来决定计算顺序:

更多关于计算的内容可以看 REBOL/Core Manual.

 

5. 变量(Variable)

词可以用作变量。一个词后面更上冒号就可以设置它的值:

出现一个没有使用冒号的词,他就会被计算:

一个变量可以用来保存任何类型的值。在上面的例子中,词print,if,> third
都是存了函数的变量。他们刚好已经被REBOL预定义了,但他们在其他方面和其他词没什么区别。

变量只有在定义他们的上下文中才有意义。一个上下文可以跨越整个程序或者被严格限定在某个特定的块、对象、函数中。在下面的例子中,use函数声明一个词age。Age是一个局部变量,在这个块外面,他可能就含有别的值,但是这不会影响age在块中的值。

局部变量同时是由函数创建的,下一节就会叙述他。

更多关于变量的内容可以查阅 REBOL/Core Manual.

6. 函数(Function)

一个函数就是一个块,这种块可以有局部变量,当每次计算这个块的时候,局部变量都会被给与新的值。局部变量是函数的参数。函数可以用多种方法来创建。这里是最常见的:

他会新建一个函数并设置为一个词sum。第一个块描述了函数的参数,第二个块便是当调用函数的时候所计算的块(这里,他把两个值相加并返回结果)。如果要使用这个函数:

当调用sum函数时,a的值设为2同时b的值为3。他们被相加,结果被返回,同时结果被作为参数传给了print函数。

对于某些函数,你也许需要局部变量但不要把他们作为参数。你可以使用function代替以上情况中出现的func

这个series是参数,total是一个局部变量用来进行计算的。注意在两个地方用到了return,将结果作为函数结果返回。如果没有要返回的结果,可以使用exit

当然,还有更多关于函数的东西可讲,请参看 REBOL/Core Manual.

7. 路径(Path)

路径指明了一个精化。他提供了一种简便的方法来指出需要更多的信息。你可能已经对他们作为文件和URL中的一部分很熟悉了。斜杠(/)是用来分隔路径中的词的。

在REBOL中,路经也可以对块、函数、对象和字符串使用:

下面的例子演示了使用块作为小数据库并通过路径访问的简便之处:

更多关于路径的信息可以查阅 REBOL/Core Manual.

8. 对象(Object)

一个对象是一组在特定上下文中含有值的变量。对象可以用来管理有更复杂行为的数据结构。例如,一个银行帐户可以从使用对象指定属性和方法中受益:

name, balancess-number, deposit,以及withdraw 都是account 对象的局部变量。deposit和withdraw变量是在对象中定义的局部函数。account的变量可以通过路径来访问:

要生成其他的account,并设置一个新的balance但是其他的值保持不变:

你也可以通过添加一个银行名称和最后活动日期扩展已有的account对象来新建一个account:

对象还有一些别的特性,包括隐藏变量。想要发现更多内容,请看 REBOL/Core
Manual
.

9. Messages消息

一个REBOL消息,简单的说,是一个从一个地方发送到另一个地方的文本文件。不管消息时在计算机之间、进程之间、还是人之间发送的——消息的形式都是一样的。他们由REBOL头开始,标示了内容、日期和其他属性:

头可以包含其他信息,由消息的目的和重要性决定。头接下来跟着的是消息的内容本身。就像上一节所显示的,他可以包含数据、代码或者两者的混合。

消息和在已有的互联网协议如http, mail
(SMTP & POP), new 和
ftp上传送。消息也可以被嵌在别的内容形式中,例如网页或者文档文件。在下面的例子,REBOL从一个网站读取一个消息并且计算它:

这个例子通过EMAIL发送一个简短的REBOL消息:

这个例子会检查每一个发送到你邮箱的消息是否从其他的REBOL程序中发出的:

import 函数把EMAIL消息转换成REBOL对象以便访问。如果email包含一个x-rebol行,那么它就是从REBOL发出的。

 

10. 方言(Dialect)

方言是携带了以不同的值和词的语法(顺序)的使用方式产生的精炼了的含义。方言一般是唯一的并且针对他们所设计解决的问题是十分合适的。

其实你已经看到了一写简单的方言。例如,一个函数的参数列表就是一种方言,除了那些已经在这里显示了的还包含很多种变体。其他的方言也可以用来分析表达式和格式化输出。

这里有一些其他方言的例子,可以用来解决特殊的问题。第一个是影像工作室控制脚本::

一个图形用户界面:

足球赛程:

安装脚本:

这有无穷无尽的可能性。更多关于方言的介绍将会在REBOL继续开发之前几个月提供给大家。

REBOL到底哪里不同?

翻译:ShiningRay @ NirvanaStudio

REBOL并非一个传统的计算机语言像C、BASIC或者Java。相反,REBOL是设计为解决在计算中的一个根本问题:在分布的计算机系统之间的信息的交换和解释

REBOL并不是通过综合一种编程语言和一种元数据语言的概念来解决这个问题的。就好像你也可以直接在C/C++中表达和处理元数据或者在XML中书写代码算法一样。解决这个问题是通过相关表达式relative expression)的概念(这也是REBOL名称的由来——Relative
Expression Based Object Language)。按照技术术语来说,REBOL是一种元循环语言meta-circular language)。也就是,REBOL扮演了自身的元语言。

相关表达式,也叫做“方言”,给代码和数据的表示提供了极高的效率,同时,他们也是REBOL最强大的力量。比如,REBOL可以不仅仅可以只用一行代码就建立一个图形用户界面,还能把这一行作为数据发送出去在世界上的另一台网络计算机系统上进行处理和显示。

REBOL的一致的架构提供了广泛而且强大的能力,从他小巧的解释器(称为REBOL/Core)到一整套互联网操作系统(Internet Operating System ,称之为REBOL/IOS)。

rebol_layers.gif

REBOL的最终的目标是要为一切连接在互联网上的设备之间信息的存储、交换和处理提供一种新的架构。不像其它的方法需要几十兆代码,一层又一层的复杂结构,又只能在单一平台上运行,需要特定的编程工具,REBOL很小巧、很轻便、可移植并且易于管理。

REBOL应用程序(称为“reblets”)的大小很重要。不仅仅因为可以让他们只要几秒钟就下载完(即使使用调制解调器或者是无线网络)还因为reblets很小,他们就很容易创建、修改和增强。大多数REBOL应用程序,即使是很重要的能做很多事情的应用程序像建立网站、处理信用卡或者共享一个日历等等,都仅仅只有10-30KB。

例如,我们用来展示REBOL的图形化幻灯片展示程序只有9KB,他的效果像这样:

REBOL的图形化幻灯片

这里还有另外一个例子。这个用户状态和活动监视器只有7KB:

用户状态和活动监视器

还想看更多的例子么?这里有几个不同的例子,用REBOL/View写的,这里还有一些“reblets”为IOS写的。几乎所有这些加起来都不超过30KB,但他们效率仍然很高,而且是分布式应用程序。

REBOL脚本快速入门

By Carl Sassenrath REBOL Technologies

翻译:ShiningRay @ NirvanaStudio


目录

  1. 介绍
  2. 数据类型的世界
  3. 集成网络操作
  4. 创建脚本文件

1. 介绍

REBOL是针对互联网通讯设计的。REBOL是一种高级解释语言允许你访问和控制互联网的资源,而且他的便捷让你可以开始考虑把互联网当成你的个人操作系统。你已经熟悉了如何使用浏览器在网上冲浪。有了REBOL,你可以编写互联网。

在REBOL总部我们用REBOL建立手册和文档(比如当前这个),生成全部我们的网站、自动上传页面和图像到网络上、发送邮件通知、处理信用卡、远程共享文件、跟踪客户订单、进行即使通讯、主持电子会议、自动更新软件、运行技术支持、管理我们的错误数据库、删除垃圾邮件、备份服务器、遍历网络、过滤网页广告、还有——最重要的——在42种不同的系统上构建REBOL。

每天我们在几十台机器上用数百个REBOL脚本给我们带来了极大的便利。我们只要一种语言就能完成所有的事情。就像一个神奇的魔术师,REBOL使我们这个小小的小组事倍功半。

这篇指南提供了使用REBOL的简要的介绍。如果你正在慢慢得阅读那份720页的REBOL手册,我们也很理解,谁会有时间把它全部看下来呢?抱着这种想法,这篇文档的目标是在20分钟之内给你一个REBOL的大体概念并且给出一些可以找到更多资料的链接。


2. 数据类型的世界

语言是建立在一系列基本的数据类型的基础上的,他们被用来构建一切的其他东西。整数和字符串都是数据类型的例子。大多数语言有大约5种或者6种数据类型。相反,REBOL有45种数据类型。这种结果是很多任务下你可以关注于你的程序要做什么,而不是如何去做。

例如,所有的语言都可以让你对数字相加:

然而,在REBOL中,你可以一样很容易的加减时间、日期、货币、元组和坐标。这里有一些例子,还有他们的结果:

注意你不需要包含(include)任何文件,使用引入(import)语句,或者进行函数声明。他们是语言中的基础部分。

同时注意你使用同一种记号来相加各种类型。大多数函数都可工作于多种数据类型上。他们是多态的;他们可以和多种数据类型一起工作。结果是什么?你将变得更高效,却无需让你的程序变得更复杂。简单的东西就应该让他简单。

这里是另一个例子,它搜索第一个字符串中出现的第二个字符串:

同样的搜索操作也可以操作其他数据类型,比如电子邮件地址,URL,文件,标签,序列号,二进制和块:

对REBOL的其他35种数据类型感到好奇了?在控制台下面输入这一行来把他们都列出来:

过着看一下REBOL指南的值附录


3. 集成网络操作

作为“互联网操作系统”的脚本语言,你需要能访问互联网的协议就像操作文件一样简单。REBOL已经内置了14种标准协议,也不需要包含文件或者函数声明。你只要使用他们。

例如,要从你的硬盘上读取并显示一个文本文件,你可以写:(注意百分号在REBOL中表示他是一个文件名。这必须出现来防止REBOL把他认作是一个REBOL单词。)

你可以一样简单的使用各种不同的协议从网路上读取文本。下面几行例子展示了他做了什么:

如果你自己要实验这些代码,只要在这些read的前面加上一个print并且给出一个有效的URL信息。注意如果你使用了一个防火墙,你需要设置你的REBOL代理设置来看到结果。当然,这些例子仅仅是一个开始。还有很多其他特性可以使用。例如,要从一个网站读取一个图像并且把他存到磁盘上的一个文件中:

write函数使用了一个/binary精化来指明这是一个二进制文件,不是一个文本文件。

什么是“精化”(Refinement)?
一个“精化”(refinement)指定了一个函数的变体。精化以一个斜杠(/)开始同时一个函数可以包含多个精化。要了解一个函数有哪些可用的精化,在命令行下输入help并在后面加上函数名称。例如:

将会给出write函数的描述和他的参数以及精化。

如果你想将一个文本文件作为邮件内容发送出去,你可以写:

而且甚至,你可以一样简单的发送一个网页:

当然,如果你要做的只是发送一个文本消息,你可以写:

要写一行可以读取一个图像文件并且上传到一个Internet服务器的代码,你可以这样写:

注意你必须给出用户名和密码作为URL的一部分。

你是不是想上传一整个目录的文件?这个脚本可以完成这个工作:

注意这里用到了site单词的另一个精化。当一个精化是用在URL上的,他会把这个URL和他提供的文件名组合在一起。file前面的冒号是在说“获取文件名”。添加一个print到循环中来看看结果:

也许你只想要上传目录中的JPEG文件。用find检查文件名就像前面一节演示的那样:

如果要上传所有的GIF和JPEG文件,你可以写成:

现在,让我们设想你只想上传那些在两天内修改过的文件:

变量date被设置为当前的日期和时间减去两天。再用他和文件和文件的日期——通过modified?函数来获取——相比较。

你可以很简单地写出只传输小于一定大小的文件将比较的一行代码换成:

或者,你可以避开子目录:

这些函数也可以和URL一起工作。你可以检查远程文件的大小:

你也可以检查文件的修改日期and you can check the file modification date with:

这一层次上的互联网集成让你可以在你的程序中十分简单的操作网络资源。例如,在REBOL/View中如果你想建立一个窗口并使用一个来自互联网上的图像文件,你可以写成:

这个的结果是产生了一个窗口显示图像像这样:

intro1.png

如果你想改变窗口的颜色并且把图像放到一个按钮中,点击之后浏览一个网站,这里是扩展的例子:

他会看起来像:

intro2.png

类似的,你可以把前面的网络函数添加到这个脚本中。例如,这个按钮可以进行FTP上传或者下载、接收邮件、发送邮件,监测新闻组,获取一个用户的信息,和任何其他的事情。

最后,要展示REBOL和互联网的完整的集成,你可以直接从网络上运行其它的REBOL程序。他们可以从以上任何一种返回一个有效的脚本的协议中运行。例如,这里是一个他如何从一个网站上运行一个脚本:

do函数下载脚本并运行它。要从FTP运行它,你可以写成:

你现在可以对REBOL如何“编写互联网”有一个更好的概念。作为最后一个例子,我们结合了图像按钮和do例子:

当你点击图像的时候,他会启动RIM,REBOL即时通讯软件。你对RIM好奇吗?试试这个:

这个即时通讯软件怎么这么小呢?看看代码:

你就知道了。

要了解网络编程的更多内容,请阅读REBOL指南的网络编程一章。


4. 创建脚本文件

REBOL文件可以用作代码、数据或者是两者的结合。这些文件可以用任何文本编辑器建立,或者他们可以作为其他程序的运行结果创建。例如,我们公司使用REBOL文件来储存很多我们每日使用的数据库。

每个REBOL文件以一个标识他的内容、作者、版本、历史和各种其他可选的字段的头部。这个信息十分有用,我们也鼓励你在你的每个脚本中给出他。他将帮助大家了解:

  • 这个脚本做什么
  • 谁写的
  • 他现在什么版本
  • 他最后什么时候修改的
  • 谁拥有它
  • 他应该使用什么文件名

这个脚本头总是用REBOL自身来描述。他们是REBOL可以读取的元数据。这允许基于REBOL的搜索引擎和存档服务更方便得建立有用的文件索引。另外,智能的文本编辑器和代码版本软件可以自动跟踪版本。

这里有一个最小化头的范例,你可以用于上面演示的任何脚本中:

注意日期和版本并没有加引号。他们是按照REBOL的数据类型来写的。前者是date类型,后者是一个元组tuple(注意它必须要有三个部分)。

从前一节中拿出最后一个例子,这里是完整的例子脚本文件:

要了解跟多REBOL脚本和头信息的资料,请参考REBOL指南的脚本一章。

简易Smalltalk测试框架

简易Smalltalk测试框架:
以及模式

Kent Beck,
First Class Software, Inc.
KentBeck@compuserve.com

翻译:ShiningRay @ NirvanaStudio

本软件和文档是由编码社区提供的服务。你可以随意分发。First Class Software, Inc.不提供任何形式的声明或隐含的担保。

(Transcribed to HTML by Ron Jeffries. The software is available for many Smalltalks, and for C++, on my FTP site.)

介绍

Smalltalk语言曾饱受煎熬,因为他一直缺少一种测试的习俗。这篇专栏将介绍一种简单的测试策略和框架来支持我们的Smalltalk。虽然这个测试策略和框架并不打算发展成为一个全套的解决方案,但是他相当于一个起点,任何有工业强度的工具和过程都都可以从他起进行建造。
本论文分为三个部分:

  • 原理 – 描述了书写和运行由框架体现的测试原理。阅读这一章了解基本的背景。
  • 手册 – 一个简单模式系统可以用来写你自己的测试。
  • 框架 – 测试框架的一个简单版本。阅读这一章可以深入了解框架是如何操作的。
  • 例子 – 一个使用测试框架来测试Set对象中方法的例子。

原理

我不喜欢基于用户界面的测试。在我的印象中,基于用户界面脚本的测试是十分脆弱的,难于使用。当我还在参加一个项目,我们使用了 用户界面测试,每天早上送来的测试报告中有二三十个测试失败,那是常有的。随便检查一下就会发现大多数的失败中实际上程序还是按照期望的运行的。界面上稍作一些修饰,就会导致最终的输出和期望的输出不一样。我们的测试元花费太多的时间保持测试代码和现有代码同步并且找出那些“伪失败”和“伪成功”,却没多 少时间写新的测试。

我的解决方法是直接在Smalltalk中写测试代码并检查他的结果。这个方法有一个缺点就是你的测试员必须会写一些简单的Smalltalk程序,但因此测试就变得更为可靠。

失败和错误

这个测试框架会区分失败和错误。一个失败指预期可能发生的问题。当你写测试时,你要检查预期的结果。如果你后来得到的是一个不同的结果,这是一个失败的测试。有一个错误那就更为悲惨,你可能根本没检查到有这个错误情况的可能。

测试单元

我推荐开发人员自己写自己的单元测试,每个类写一个。测试框架也支持写测试套件(TestSuite,或叫测试序列),也可以附加给一个类。我还推荐所有的类响应“testSuite”消息,返回一个包含单元测试的测试套件。另外,我还推荐开发人员花20~50%的时间开发测试代码。

集成测试

我推荐由一个单独的测试员写集成测试。哪里运行集成测试呢?最近的用户界面框架运动到更好的程序访问提供了同一个答案——驱动用户界面,但让测试来做。在VisualWorks(下面的实现也会用到这个方言),你可以打开一个应用程序模型ApplicationModel并不断往他的ValueHolders中填入值,造成各种混乱,也不会产什么麻烦。

运行测试

这是原理部分的最后一点。有种做法很有诱惑力——建立一系列测试数据,然后运行一系列测试,然后清除。在我的经历中,这个要比他代来的价值造成更多的问题。一个测试可能会结束和另一个的交互,而且一个测试中的失败可能造成后续的测试都不能够运行。测试框架让我们可以很容易的建立一系列通用测试数据,但是这些数据会在每次测试之前建立并在之后抛弃。这种做法的潜在的性能问题不会造成很大的影响因为测试可以在无人值守的情况下运行。

手册

这里是一个书写测试的简易模式系统。有以下模式:

模式

目的

装置

建立一个通用的测试装置。

测试案例

建立一个测试案例的激发器。

检查

检查测试案例的结果。

测试套件

聚合TestCase测试案例。

装置(Fixture)

你怎样开始写测试呢?

测试是一种不可能的任务。你要让他完全通过,这样你才能保证软件可以正常工作。从另一角度看,由于你的程序的可能状态太庞大,以至于你不可能测试每一种组合情况。

如果你一开始很茫然,都不知道你要测试什么,你就永远不能进入测试的状态。由一个行为可预测的配置开始则要好很多。当你对你开发的软件有更多的了解,你可以加入一个配置列表。

这样一个配置,称之为“装置”(Fixture)。下面是装置的例子:

装置

预计反馈

1.0 and 2.0

算数问题很容易给出预计的答案

到已知的机器的网络连接

对网络包进行相应

#() and #(1 2 3)

发送测试消息的结构

选择一个装置你判断哪些要测试哪些不要。一个完整的测试会有很多装置,每一个都可以以多种途径进行测试。

设计一个测试装置。

  • 创建 TestCase 的子类
  • 在装置中给每个已知的对象添加一个实例变量
  • 重写 setUp 来初始化变量

在下面的例子中,测试装置是两个Set(集合),一个为空另一个包含元素。首先我们给TestCase建立子类并给对象添加实例变量,以便我们以后进行引用:

Test Case测试案例

你已经有了一个装置,下面要做什么呢?

你如何表现测试的一个单元?

你可以预测给装置发出消息之后的结果。你需要通过某种方式表现这样一种预知的情况。

最简单的方法是用交互的方法表现。你打开装置的一个检查器(Smalltalk中的Inspector)
并且给他发送消息。这种做法有两个缺点。第一,你要一直给同一个装置发送消息。如果有一个测试刚巧把对象弄乱了,那么后面所有的测试都会失败,即使代码也许是正确的。更重要的是,你不能方便地和其他人进行交流,交换你的交互测试。如果你把你的对象给了其他人,他们唯一可以测试这些对象的办法就是把你叫来并检查他们。

通过将每一个可预测的情况表现为一个对象,每一个都有一个自己装置,没有哪两个测试会互相干扰。同时,你可以很方便地把测试给别人运行。

用一个方法来代表装置的一个可预见的反应。

  • 给TestCase的子类添加一个方法
  • 用该方法激活装置

下面的例子代码演示了这两点。我们可以预见:将“5”加入一个空的集合会得到“5”在集合中的结果。我们给我们的TestCase的子类添加一个方法。用它来激活装置:

一旦你激活了装置,你需要添加一个检查机制来确保你的预期正确发生。

检查

一个测试案例会激发一个测试装置。

你如何测试要得到预期的结果?

如果你在进行交互测试,你可以直接检查预期的结果。如果你要查看一个特定的返回之,就是用“print it”,并确保你获得正确的对象。如果你要查看一些额外的影响,请使用Inspector检查器。

由于测试是在他们自己的对象中,你需要一种直接用程序查找问题的方法。一种实现办法是使用标准错误处理机制(Object>>error:),以测试逻辑为error:信号

当你在测试的时候,你可能想要区别你要检查的错误,象二加三得六,和那些你没有预见的错误,如下标越界或者是未定义的消息。

你还没什么办法对付无法预见的错误(如果你已经对他们作了些什么,他们就不会说无法预见了,是不是?)当一个灾难性的错误降临时,测试框架会停止运行测试案例,记录错误,并直接运行下一个测试案例。由于每一个测试案例都有他自己的装置,前一个案例中的错误不会影响下一个。

测试框架简单的提供了一个“should:”方法来检测预期的值,这个方法带一个语句块作为参数。如果语句块得出的是真,那一切都好。否则,测试案例停止运行,测试失败被记录在案,然后运行下一个测试案例。

把检查的内容变成一个返回值为布尔型的语句块,并把语句块作为参数传递给“should:”。

在下面的例子中,在通过给一个空集合加“5”激活装置之后,我们希望检测并确保里面包含它。

有一个TestCase>>should: 的变体。TestCase>>shouldnt: 在语句块参数为真的情况下失败。这样你就不必使用“(…)not”了。

你一旦有了一个测试案例,你就可以运行它了。给你的TestCase的子类创建一个实例,并给出测试方法的选择器。并发送“runt”给结果对象:

如果他运行结束,那么测试通过了。如果摔了个跟头,看来有什么东西出错了。

TestSuite测试套件

你现在有若干测试案例。

你如何一下运行许多测试呢?

一旦你有两个测试案例在运行,你会希望他们能一个接一个运行而不是你去执行两次“do it”。你可以就把两块运行表达式放弃及然后执行来运行测试。然而,如果当你又需要运行“这一块案例和那一块案例”的时候呢,你就会烦了。

测试框架提供了一个对象来表示“一系列测试”——测试套件TestSuite。一个TestSuite运行一套测试案例并且把他们的运行结果同时汇报上来。根据多态的优点,TestSuite同样可以包含其他TestSuite,所以你可以把Joe的测试和Tammy的测试放在一个创建一个更高一级的套件。

把测试案例组合到测试套件中。

给一个TestSuite发送了“run”的结果是一个TestResult对象。他记录了所有的测试案例引发的失败或错误,和套件运行的时间。

所有这些对象都是可以通过ObjectFiler或者BOSS进行储存。你可以很方便的保存一个套件,然后调入并运行,和以前的结果进行比较。

Framework框架

这一部分展示了测试框架的代码。如果你对框架的实现很好奇,或者你想要修改它,那么就在这一章了。当你和一个测试员交谈时,他们谈论的测试的最小单元是测试案例。TestCase是一个用户的对象,代表了一个单个测试案例。

测试员谈论如何设置一个“测试装置”,这是一个带有可预测响应结构的对象,他既容易建立也容易推导。几个不同的测试案例可以针对同一个测试装置。

这个差别表现在框架中是通过给每个TestCase一个可插入的选择器。选择器调用的变量行为便是测试代码。同一个类的所有实例共享同一个装置。

TestCase class>>selector: 是一个 Complete Creation Method

TestCase>>setSelector: 是 Creation Parameter Method

TestCase的子类需要相应改写钩子方法setUp和tearDown来建立和销毁测试装置。TestCase本身提供了两个什么也不做的基本方法。

运行一个TestCase最简便的方法就是向他发送“run”消息。Run调用设置代码,运行选择器,然后运行拆卸代码。注意拆卸代码不管执行测试的时候有没有错误都会执行。调用setUp和tearDown可以封装成Execute Around Method,不过由于他们不是公共接口的一部分,他们必须在这里编写。

PerformTest 仅仅执行一下选择器

单个的TestCase一点意思也没有,一旦你让他运行起来了之后。然后,你就会想一次运行很多很多测试案例。测试员谈论运行测试“套件”。TestSuite是一个用户对象。它是测试案例的组合。

TestSuites 是有名称得对象。因此可以很方便判断他们的身份,这样他们可以被储存在存储器中,也可以从中读取。一下是完整的构造方法和构造参数方法。

testCases实例变量是在TestSuite>>setName: 中初始化的因为我不需要让他变成其他类型的集合。

测试套件有一个名称的访问方法,这样可以在用户界面上显示他们。

当你行一个TestSuite,你可能想要运行它所有的TestCase。但他不是这样简单的。如果你有一个代表应用程序的验收测试侧套件,在运行之后,你可能还想知道套件运行了多久,哪个案例有问题。这些是你可能想要储存以便将来引用的信息。

TestResult是一个TestSuite的结果对象。运行一个TestSuite返回一个TestResult,记录了上面描述的信息——起始时间和中止时间,套件名称和所有的失败和错误。

TestCase>>run 和 TestSuite>>run 在多态上并不相同。这是需要在以后的框架中解决的问题。一种做法是用一个可以按微秒度量的TestCaseResult来做性能回退测试(performance regression testing.)。

默认的TestResult由TestSuite构造,使用一个 Default Class.

一个TestResult Complete Creation Method 需要一个 TestSuite.

TestResult可以通过发送start和stop消息来做时间标记。由于start和stop需要成对执行,他们必须隐藏在一个Execute Around Method方法中。这也是以后要做的。

当一个TestSuite运行时给出一个TestResult,它仅仅将其中每一个TestCase带着TestResult运行。

#run: 是TestSuite和TestCase中的组合选择器,这样你可以构造包含其他TestSuite的TestSuite,替代或者补充TestCase。

当一个TestCase运行时给出了一个TestResult,它应该要么安静无误得运行,向TestResult中添加一个错误,或者添加一个失败。捕获错误简单的使用了系统提供的errorSignal。捕获失败必须由TestCase本身提供。首先,我们需要一个 Class Initialization Method 来创建一个信号。

现在我们需要一个 Accessing Method.

现在,当TestCase用一个TestResult运行时,它要捕获错误和失败然后通知TestResult,同时它必须运行tearDown代码不管测试是否正确执行。者造成了这是框架中最丑的方法,因为有两个嵌套的错误处理器和方法中的valueNowOrOnUnwindDo:。这里少解释了一个模式在TestCase>>run 关于使用ensure:确保安全运行Execute Around Method 的第二个停止。

当一个TestResult被告知有一个错误或者失败发生了,他在他其中一个集合中记录这个事实。为了简单起见,记录只是两个数组,但是他可能应该是一个类对象包含一个时间标签和问题更详细的信息。

一旦在测试方法中出现一个未捕获的错误(例如,无法辨认的消息),就调用错误的情况。失败的情况如何调用呢?TestCase提供了两个方法简化了失败检查。第一个是should: aBlock,如果aBlock执行后返回假便发出失败消息。第二个,shouldnt: aBlock,和前面刚好相反。

测试方法将会运行代码来激发测试装置,然后在should:shouldnt:块中检查结果。

例子

Ok, 这就是他的工作原理,你怎么来用它呢?这里有一个简短的例子测试Set支持的一些消息。首先我们建立TestCase的子类,因为我们总想有好几对有趣的集合可玩。

现在我们需要初始化这些变量,所以我们重写setUp。

现在我们需要一个测试方法。让我们测试看看给一个集合添加元素究竟能否成功。

现在我们可以通过执行”(SetTestCase selector: #testAdd) run”来运行一个测试案例。

这里有一个使用shouldnt:的情况。他这么念的”after removing 5 from full, full should include #abc and it shouldn’t include 5.”

这里又有一个特殊情况,确保在你要使用索引访问的时候发出错误信号。

现在我们可以把它们一起放在一个测试套件里面。

这里是这个套件的对象浏览器图TestResult是我们运行之后得到的。 

以上所展示的测试方法仅仅覆盖了Set中的一小部分功能。给Set中所有的公共方法写测试确实是一个很让人郁闷的任务。尽管如此,就像 Hal Hildebrand 在使用了这个框架的早期一个版本之后告诉我的那样。“如果根本的对象不能正常工作,其他一切都免提。你必须写测试来确保所有的东西都能正常工作!”

嵌入JavaScript引擎梗概教程

嵌入JavaScript引擎

梗概教程

作者:Brendan Eich

2000年2月21日

翻译:ShiningRay @ NirvanaStudio

如何启动VM并执行一个脚本

如果不使用任何错误检查这样:

JS_起头的返回指针的函数会返回空(null)

JS_起头的返回布尔值的函数会返回假(false)
(错误照例会被保存在一个JSBool变量ok中)。

如何从JavaScript中调用C函数

假设有个C函数叫diot,他在被调用时需要至少两个实参(如果调用者少提供了几个,JS引擎需要保证undefined值传给了那些缺少的参数):

然后把它和JS连接起来,你要写:

或者,如果你有一堆的本地函数要调用,你可以把它们放在一个表格中:

(最后,全为0的函数表示表格结束)并且用:

ok = JS_DefineFunctions(cx, global, my_functions);

如何从C中调用JavaScript函数(像“onClick”)

假设点击事件是发生在最顶层或者是有焦点的UI元素,位置为(x,y):

再次声明,我省略了错误检查(例如在调用之后检查!ok),同时我伪造了一些C的事件管理程序来模拟DOM的协议——如果他的处理程序返回假就取消这个事件。


原文地址:http://www.mozilla.org/js/spidermonkey/tutorial.html

JavaScript-C引擎嵌入开发指南

JavaScript-C引擎嵌入开发指南

翻译:ShiningRay@Nirvana Studio原文地址:http://www.mozilla.org/js/spidermonkey/apidoc/jsguide.html


JavaScript-C引擎概览

本文档提供了一个JavaScript(JS)引擎的C语言实现的概述,他介绍了你如何在你的应用程序中嵌入脚本引擎来让它们可以使用JS。有两大理由让你在应用程序中嵌入JS引擎:使用脚本来自动操作你的应用程序;同时使用JS引擎和脚本无论何时都可以提供跨平台的功能并消除了应用程序解决方案对平台的依赖性。

受支持的JavaScript版本

本JS引擎支持从JS 1.0版到JS1.4。JS 1.3和更高版本符合ECMAScript-262规范。JS引擎解析、编译和执行包含JS语句和函数的脚本。这个引擎可以处理要用来执行脚本的JS数据类型和对象内存分配,同时它可以清除——垃圾回收——内存中已经不需要的数据类型和对象。

你如何使用这个引擎?

通常,你将JS引擎作为一个共享的资源进行构建。例如,在Windows和Windows NT上,这个引擎是一个DLL文件,在Unix上是一个共享库。然后你把你的应用程序和他连接,同时嵌入式JS引擎应用程序编程接口(API)就可以在你的应用程序中调用了。JS引擎的API提供了以下几种分类的函数:

  • 数据类型操作

  • 运行时控制

  • 类和对象创生的维护

  • 函数和脚本执行

  • 字符串处理

  • 错误处理

  • 安全性控制

  • 调试支持

你在每个嵌入了JS调用的应用程序中将会用到这些功能分类中的某些部分,象运行时控制和数据类型操作。例如,在你调用其他JS功能之前,你必须通过调用JS_NewRuntime函数来新建和初始化JS引擎。其他功能分类,像安全控制,提供一些可选的特性,你可以根据需要在你的应用程序中使用它们。

这个引擎和应用程序有什么关系?

从概念上来讲,JS引擎在你的系统上是一个共享资源。通过在你的应用程序中嵌入引擎API命令你可以向JS引擎传递处理的请求。这个引擎,反过来,处理你的请求,并把返回值或者状态信息返回给你的应用程序。图1.1描述了它们一般的关系:

图 1.1

图1.1

例如,假设你正在使用JS引擎来使你的应用程序能通过JS脚本自动运行,同时假设你的应用程序运行一个脚本来对一个用户进行身份验证并且设置一个用户对这个应用程序的访问权限。首先,你的应用程序可能新建一个代表用户的自定义JS对象,包括了用户的名字、ID、访问权限和一个可能的用户拥有权限在应用程序中使用的函数的列表。

在这个情况下,你的应用程序给JS引擎发送的的第一个请求可能是对JS_NewObject的调用来新建一个自定义对象。当JS引擎新建了这个对象,他返回一个指针给你的应用程序。你的应用程序可以再次调用JS引擎来执行使用这个对象的脚本。例如,在建立了用户对象之后,你的应用程序会立刻给JS_EvaluateScript传递一个脚本来立刻编译执行。那个脚本可以获得并验证用户信息,然后建立用户对其他应用程序特性的访问权限。

事实上,你的应用程序和JS引擎之间的关系远比图1.1中显示的要复杂的多。例如,它假设你已经为你的平台构建了JS引擎。它还假设你的应用程序包含了jsapi.h还假设应用程序对引擎进行的第一个调用已经初始化了JS运行时。

当JS引擎接受到了一个初始化的请求时,他会为JS运行时分配内存。图1.2描述了这个过程:

图 1.2

图 1.2

这个运行时是一个内存空间,在其中可以维护你的应用程序所使用的变量、对象和上下文。一个上下文是指,针对JS引擎所使用的线程的脚本执行状态。每个同时存在的脚本或者线程都必须有它自己的上下文。一个单独的JS运行时可以包含很多上下文、对象和变量。

几乎所有的JS引擎调用都要求有一个上下文的参数,所以在创建了运行时之后你的应用程序首先要做的一件事情是调用JS_NewContext来至少创建一个上下文。实际你需要的上下文数量由你的应用程序中所期望同时运行的脚本的数量决定。从另一方面说,如果同一时间只有一个脚本被编译执行,那么你就知需要建立单独的一个上下文,你可以对每个脚本重复使用它。

在你新建了上下文之后,你会通常想要初始化引擎内置的JS对象,可以通过调用JS_InitStandardClasses实现。内置的对象有Array,Boolean,Date,Math,Number,和String字符串对象,大多数脚本都会用到。

大多数应用程序也要用到自定义JS对象。这些对象是特定于你的应用程序的。他们通常代表了数据结构和应用程序中脚本使用的方法。要新建一个自定义对象,你要组装一个JS类来生成这个对象,调用JS_InitClass来在运行时设立这个类,然后调用JS_NewObject来在引擎中新建你这个自定义对象的实例。最后,如果你的对象有一些属性,你也许要通过调用JS_SetProperty来设置他们的默认值。

即使你在创建一个对象的时候给JS引擎传递了一个特定的上下文,最后这个对象还是独立于这个上下文存在的。任何脚本都可以和任意上下文相关联来访问任何对象。图1.3描述了脚本和运行时、上下文以及对象之间的关系。

图 1.3

图1.3

如图1.3所示,脚本和上下文完全是互相独立存在的及时他们可以访问相同的对象。在给定的运行时中,一个应用程序可以任意未分配的上下文来访问任何对象。也可能有时你想确保能为独占的使用而保留某些上下文和对象。在这些情况下,给你的应用程序新建单独的运行时:一个针对共享上下文和对象,另一个(或者更多的,取决于你的应用程序的需求)针对私有的运行时和对象。

注意:同一时间只能有一个线程被授权访问特定上下文。

构建引擎

在你可以在你的应用程序中使用JS之前,你必须将JS引擎构建成一个可共享的库。在大多数情况下,引擎代码已经包括了Make文件来自动构建。

例如,在Unix下,js源代码目录包括了一个基本的Gnu Make文件——Makefile.ref和一个config目录。config目录包括了平台特定的.mk文件来配合Makefile.ref对你的环境进行构建。在Windows NT下,NMake文件是js.mak

请阅读源代码目录中任何的readme文件,也许其中包括了和更新的编译指导或者其他信息。

嵌入引擎有什么必要条件?

如果要让你的应用程序可以执行JS,就要在你的应用程序代码中嵌入合适的引擎。嵌入一般有五步:

  1. 在你的C模块中加入#include jsapi.h来确保编译器知道有哪些引擎的API可以调用。极少部分特殊的JS引擎工作时会要求你包含额外的头文件。例如,要在你的应用程序中调用JS调试器,你要在合适的模块里面包含jsdbgapi.h

    大部分在JS源代码中的其它的头文件应该被引用。这样做可能会使你的程序依赖于引擎内部的接口,而这些接口可能随着版本发布而更改。

  1. 在你的应用程序中提供支持结构和变量声明。例如,如果你打算给JS引擎传递一个脚本呢,提供一个字符串变量保存了你的应用程序的脚本的版本的文字信息。使用jsapi.h中定义的JS数据类型来声明结构和变量。

  2. 使用JavaScript编写特定应用的对象。这些对象常常会与操作在你C程序中的结构的结构和方法进行通讯,特别是如果你在使用JS引擎来自动操作你的应用程序。

  3. 在程序代码中嵌入合适的JS引擎API调用和变量引用,包括初始化内置JS对象,和创建组成任何应用程序要用的自定义对象。

  4. 大多数JS引擎调用都会返回一个值。如果这个值是零或者空,它通常表示一个错误的情况发生了。如果值非零,它一般表示成功;在这些情况下,返回的值常常会是你的程序需要使用的指针,或者存起来留以后引用。很重要的是,你的程序至少应该每次检查JS调用返回的值。

以下代码片断描述了嵌入使用的大部分过程,除了JS脚本的建立,这点也不在本文的介绍范围之内。如要查询有关创建脚本的信息——JavaScript这个语言——请看客户端JavaScript指导,如果要得到关于编写服务器端对象,见服务器端JavaScript指导

.
.
.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

/* 包含JS引擎API头文件 */
#include "jsapi.h"
.
.
.

/* 主程序建立全局JS变量,包括运行时,
* 一个上下文,和一个全局对象,然后初始化JS运行时,
* 并创建一个上下文. */

int main(int argc, char **argv)
{
int c, i;
/*建立全局的JS变量,包括全局和自定义对象 */

JSVersion version;
JSRuntime *rt;
JSContext *cx;
JSObject *glob, *it;
JSBool builtins;

/* 初始化JS运行时,并返回结果给rt */
rt = JS_NewRuntime(8L * 1024L * 1024L);

/* 如果rt没有值,结束程序 */
if (!rt)
return 1;

/* 新建一个上下文并且把它和JS运行时相关联 */
cx = JS_NewContext(rt, 8192);

/* 如果cx没有值,在此结束程序 */
if (cx == NULL)
return 1;

/* 新建全局对象 */
glob = JS_NewObject(cx, clasp, NULL, NULL);

/* 初始化内置JS对象和全局对象 */
builtins = JS_InitStandardClasses(cx, glob);

.
.
.

return 0;

}

这个范例代码十分简单,它描述了嵌入JS引擎调用所必须的关键元素。如果想要更完整的例子——也就是以上这段代码片断的出处——参见js.c,这个范例应用的源代码是包含在JS引擎的源代码中的。

理解关键嵌入开发概念

大多数你要创建的JavaScript应用,你都会想要遵循一些权威的的JS API嵌入实践。以下部分讲述了任何程序中都要使用到的API调用。

在很多情况下,嵌入某些特定API调用的顺序决定这个程序的成功。例如,你必须在调用其它JS引擎函数之前初始化一个JS运行时。相应的,你要在关闭程序之前释放JS运行时。因此,典型的程序的main函数像一种三明治,在任何你提供的功能前后先初始化最后释放JS运行时:

int main(int argc, char **argv)
{
int c, i;

/*建立全局JS变量,包括全局对象global和自定义对象 */
JSVersion version;
JSRuntime *rt;
JSContext *cx;
JSObject *glob, *it;

.
.
.

/* 初始化JS运行时,并把返回的结果放在rt中 */
rt = JS_NewRuntime(8L * 1024L * 1024L);

/* 如果rt没有值,程序结束。 */
if (!rt)
return 1;

.
.
.

/* 建立一个上下文 */
cx = JS_NewContext(rt, 8192);

/* 如果cx值为空,则结束程序 */
if (cx == NULL)
return 1;

/* 初始化内置JS对象和全局对象 */
builtins = JS_InitStandardClasses(cx, glob);

.
.
.

/* 把你的代码扔这里,包括你要用来创建自定义JS对象的JS API函数调用。
* JS对象模型在这里开始。 */

.
.
.

/* 在退出应用程序之前,释放JS运行时 */
JS_DestroyRuntime(rt);

正如这个例子所描述的,嵌入了JS引擎的函数的应用程序要负责建立JS运行时,这是他最先要完成的动作之一,同时它还要负责在退出之前释放运行时。一般来说,确保运行时被初始化和释放的最佳位置是在中央JS调度程序的模块中嵌入必要的调用,无论你将使用哪一个模块作为在应用程序的中央调度模块。

在你初始化了运行时之后,你可以建立应用程序的JS对象模型。这个对象模型决定了你的JS对象互相之间的关系。JS对象本质上是分层次的。所有的JS对象都是默认与全局(global)对象相关的,他们都是全局对象的子孙。当你初始化标准JS类的时候,你会自动获得一个全局对象:

builtins = JS_InitStandardClasses(cx, glob);

全局对象会建立一些基本属性和方法,其他对象都会继承这些属性和方法。当你创建你自己的对象时,他们可以自动使用这些已经定义在全局对象上的属性和方法。你可以通过在自定义对象上重新对他们进行定义来覆盖这些默认属性和方法,否则可以直接接受默认的赋值。

你也可以基于其他的内置JS对象新建自定义对象,或者基于其他自定义对象来新建对象。无论哪种情况,你新建的对象在继承链中将继承他祖先的所有属性和方法,一直追溯到全局对象。如果要了解更多关于全局和自定义对象地内容,请参见“初始化内置和全局JS对象”以及“创建和初始化自定义对象”。

管理一个运行时

JS运行时是一块内存空间,在这里面JS引擎可以管理与JS函数和脚本相关的上下文、对象和变量。在执行任何JS函数或者是脚本之前,你必须初始化一个运行时。初始化运行时的API调用是JS_NewRuntimeJS_NewRuntime有一个参数,是一个无符号整数,它指明了在垃圾收集发生之前,分配给运行时的内存最大数值,单位是字节。例如:

rt = JS_NewRuntime(8L * 1024L * 1024L);

如上面列举的,JS_NewRuntime也会返回一个值,这个值是一个指向新建的运行时的指针。一个非空的返回值表示运行时被成功创建了。

一般来说,一个应用程序只需要一个运行时。但是,你还是可以创建多个运行时的,我们可以在必要的时候调用JS_NewRuntime并把返回值存在不同的指针中。

当不再需要JS运行时的时候,应该把它销毁来释放他占用的内存资源,以便给其他应用程序来使用。根据你的应用程序中JS的使用范围,你可以选择在JS使用结束立刻销毁运行时,或者,你可以选择一直保留运行时知道你的应用程序即将结束。无论哪种情况,都必须使用JS_DestroyRuntime来释放运行时,当运行时不再需要的时候:

JS_DestroyRuntime(rt);

如果你使用了多个运行时,要确保在结束应用程序前,每一个都被正确释放了。

管理上下文

几乎所有的JS API调用都要求你传送一个上下文作为参数。在JavaScript引擎中,一个上下文唯一对应一个脚本。引擎把上下文信息传送给运行脚本的那个线程。每个同步执行的脚本必须被分配一个唯一的上下文。当一个脚本执行完之后,他的上下文就不再被使用了,这时候这个上下文就可以再次被分配给一个新的脚本,或者可以释放他。

要为一个脚本创建新的上下文,可以使用JS_NewContext函数。该函数有两个参数:一个指针指向上下文所需结合的运行时,和为上下文分配的栈空间的大小,以字节为单位。如果成功,函数返回新建的上下文的指针。例如:

JSContext *cx;
.
.
.
cx = JS_NewContext(rt, 8192);

运行时必须已经存在。你为上下文指定的栈的大小应该足够容纳使用这个上下文的脚本所创建的任何变量和对象。注意和分配和维护上下文相关有一个特定的数量,因为你可能要:

  1. 只创建同一时刻在你的应用程序中所需要的数量一样多的上下文。

  2. 只要上下文有可能被应用程序用到,就保留他们,而不是每当需要的时候再重新新建不需要了就立刻销毁。

当不再需要某一个上下文时,应该把它销毁来释放它占用的内存资源留给其他的应用使用。根据你的应用程序中的JS使用范围,你可以选择在使用完上下文之后,就立刻销毁,或者,更多情况下,你可以考虑为以后重复使用来保留上下文直到应用程序结束为止。不管哪种情况,当他不再需要用到的时候,可以使用JS_DestroyContext来释放上下文。这个函数带一个参数,也就是指向要销毁的上下文的指针:

JS_DestroyContext(cx);

如果你的应用创建了多个运行时的话,应用程序需要了解上下文和哪个运行时相关联。在这种情况下,可以调用JS_GetRuntime,并且把上下文作为参数传递给他。JS_GetRuntime会返回一个指向某个合适的运行时的指针,如果存在的话:

rt = JS_GetRuntime(cx);

当你创建一个上下文的时候,你要给他分配栈空间,这个空间将为那些被使用这个上下文的脚本所创建的变量和对象所使用。你也可以用给定的上下文仅仅用来储存大量数据,只要分配所需的最小的栈空间。调用JS_SetContextPrivate函数来建立一个指向上下文使用的私有数据的指针,并调用JS_GetContextPrivate函数来获取这个指针,这样就可以访问这些数据了。你的应用程序要负责创建和管理这个可选的私有数据。

若要创建私有数据并把它和一个上下文关联:

  1. 根据需要建立私有数据,可以使用一个普通的 C void 指针变量。

  2. 调用JS_SetContextPrivate,并指明通过哪个上下文来建立私有数据,并给出指向数据的指针。

例如:

如果要在以后获取数据,调用JS_GetContextPrivate函数,并把上下文作为参数传递给他。该函数会返回指向私有数据的指针:

初始化内置的和全局的JS对象

JavaScript引擎提供了一些内置对象,他们会简化你的某些开发任务。例如,内置的Array对象让你更方便地在JS引擎中创建和处理数组结构。类似,Date对象提供了一个统一的处理日期数据的机制。要查阅引擎支持的内置对象的完整列表,请参看JS_InitStandardClasses

JS引擎始终使用函数和全局对象。一般来说,全局对象存在于JS脚本的场景背后,为所有其它JS对象提供了一个默认的空间范围和存储了你在程序中创建和使用的全局变量。在你创建你自己的对象之前,你需要初始化全局对象。函数对象将启用对象支持和构造器调用。

JS_InitStandardClasses, 这个API调用将初始化全局和函数对象还有引擎内置的对象,这样你的应用程序就可以使用他们了:

JS_InitStandardClasses会返回一个JS boolean值来表示初始化是否成功。

你可以为你的应用程序指定一个不同的全局对象。例如,Netscape Navigator就使用了他自己的全局对象window。若要为你的应用程序更改全局对象,可以调用JS_SetGlobalObject函数。详细信息请查阅JS_SetGlobalObject的参考条目。

创建和初始化自定义对象

除了可以使用引擎内置对象之外,你还可以新建、初始化和使用你自己的JS对象。如果你使用JS引擎处理脚本对你的应用进行自动化操作,这点尤其重要。自定义JS对象可以提供最直接的程序服务,另外他们也可以作为你的程序服务的一个接口。例如,一个自定义JS对象提供了某种直接的服务,像处理应用程序所有的网络访问、作为数据服务的中间层。也可以是使用一个JS对象映射到应用程序中以后的数据和函数中,这样能为C代码提供一个面向对象的接口。这样一个自定义对象对应用程序自身来说扮演了一个接口的角色——从应用程序中把值传递给用户,并且接受和处理用户的输入然后再返回给应用程序。这种对象也可以用来对应用程序内部的函数进行访问控制。

有两种方法可以创建自定义对象:

  • 写一个JS脚本,用来创建对象,以及他的属性、方法和构造器,然后把这个脚本在运行时传递给JS引擎。

  • 在你的程序中嵌入定义对象的属性和方法的代码,调用引擎来初始化新对象,然后通过其它的引擎调用来设置对象的属性。这个方法的一个好处是你的程序可以包含直接处理所嵌对象的本地方法。

无论哪种情况,如果你创建了一个对象然后要将他在运行时中持久化,以便在此运行时中可以被其他对象调用,那么你必须通过JS_AddRoot JS_AddNamedRoot调用来确定这个对象的“根”。使用这两个函数会确保JS引擎去跟踪这些对象并在适当的时候通过垃圾收集过程中清理掉他们。

从脚本中建立一个对象

要从脚本中创建自定义JS对象的一个原因是,只需要一个在脚本运行期间存在对象。要创建这种持续在脚本调用期间的对象的话,你也可以直接在你的应用程序中嵌入对象的代码,而不用使用一个脚本。

注意:你同样可以使用脚本创建持久对象。

要使用脚本创建一个自定义对象:

  1. 定义和说明对象。他的目的是什么?他的数据成员(属性)有哪些?他有哪些方法(函数)?他是否需要一个运行时构造函数?

  2. 编写出定义和创建对象的JS脚本。例如:

    function myfun(){
    var x = newObject();
    .
    .
    .
    }

    注意:使用JavaScript编写的对象并不在应用程序嵌入JS引擎的代码中。关于对象编写的更多内容,请参阅《客户端JavaScript指导》和《服务器端JavaScript指导》。

    在应用程序中嵌入合适的JS引擎调用来编译和执行脚本。你有两种选择:1.) 仅使用一个函数调用来编译和执行脚本:JS_EvaluateScript,JS_EvaluateUCScript或者2.) 使用JS_CompileScript或者JS_CompileUCScript,来一次性编译脚本,然后可以用一个独立的函数调用JS_ExecuteScript. 来重复执行已经编译的代码。这些调用的“UC”版可以提供对统一码脚本的支持。

你使用脚本创建的一个对象只可以在脚本的生命周期内启用,或者也可以在脚本运行结束之后持久化。一般来说,一旦脚本运行结束,他的所有对象都会被销毁。在大部分情况下,这种行为正是你的应用程序需要的。然而,在其他的情况下,你可能希望某对象持续在脚本之间,或者你的应用程序的整个生命周期。这样的话你需要直接在你的应用程序中嵌入对象创建代码,或者你必须把对象直接连接到全局对象这样他会一直持续只要全局对象本身存在。

在应用程序中嵌入一个自定义对象

当必须进行对象持久化时,或者你认为需要对几个脚本都可用的对象时,嵌入一个自定义JS对象在应用程序中是很有用的。例如,一个代表了用户的ID和访问权限的自定义对象可能会在应用程序的整个生命期中都会用到。他事先一次性创建和组装了对象,节省了很多时间,而不用每次要检验用户ID或者权限时一遍又一遍用脚本创建对象。

一种在应用程序中嵌入自定义对象的方法是:

  1. 创建一个JSPropertySpec数据类型,并把它和属性的信息组装成对象的属性,包括参数的获取(get)和设置(set)方法的名称。

  2. 创建一个JSFunctionSpec数据类型,并把它和方法的信息组装成对象使用的方法。

  3. 创建一个实际的C函数用来处理对象的方法调用。

  4. 调用JS_NewObject或者JS_ConstructObject来实例化这个对象。

  5. 调用JS_DefineFunctions来创建这个对象的方法。

  6. 调用JS_DefineProperties来创建这个对象的属性。

描述持久的自定义JS对象的代码必须放在应用程序执行的开始部分附近,在任何依赖于该对象的代码之前。嵌入的实例化和组装自定义对象的引擎调用也应该出现在任何依赖这个对象的代码之前。

注意:在大多数情况下还有一个更方便的在程序代码中创建自定义对象的方法是调用JS_DefineObject来创建对象,然后反复调用JS_SetProperty来设置对象的属性。关于定义一个对象的更多的信息,参见JS_DefineObject。关于设置对象属性的更多信息,参见JS_SetProperty

为对象提供私有数据

像上下文那样,你可以把大量的数据和一个对象相关联而无需把数据存储在这个对象中。调用JS_SetPrivate来建立一个指向私有数据的指针,并且调用JS_GetPrivate来获得这个指针这样就可以访问数据了。你的应用程序要对这些可选的私有数据的创建和管理负责。

要创建私有数据并把它和一个对象相关联的话:

  1. 根据需要建立私有数据,可以使用一个普通的 C void 指针变量。

  2. 调用JS_SetPrivate, 制定要为那个对象建立私有数据,并给出指向数据的指针。

例如:

如果要以后再获取数据,调用JS_GetPrivate并且把对象作为一个参数传递。这个函数将返回一个指向对象私有数据的指针:

处理统一码(Unicode)

JS引擎现在提供了很多API函数的支持统一码的版本。这些函数允许你直接给引擎传递使用统一码编码的脚本进行编译和运行。下面的表格列出了标准引擎函数和他们对应的统一码版本:

标准函数

统一码支持函数

JS_DefineProperty

JS_DefineUCProperty

JS_DefinePropertyWithTinyId

JS_DefineUCPropertyWithTinyId

JS_LookupProperty

JS_LookupUCProperty

JS_GetProperty

JS_GetUCProperty

JS_SetProperty

JS_SetUCProperty

JS_DeleteProperty2

JS_DeleteUCProperty2

JS_CompileScript

JS_CompileUCScript

JS_CompileScriptForPrincipals

JS_CompileUCScriptForPrincipals

JS_CompileFunction

JS_CompileUCFunction

JS_CompileFunctionForPrincipals

JS_CompileUCFunctionForPrincipals

JS_EvaluateScript

JS_EvaluateUCScript

JS_EvaluateScriptForPrincipals

JS_EvaluateUCScriptForPrincipals

JS_NewString

JS_NewUCString

JS_NewStringCopyN

JS_NewUCStringCopyN

JS_NewStringCopyZ

JS_NewUCStringCopyZ

JS_InternString

JS_InternUCString

JS_InternUCStringN

处理统一码的函数工作方式与原来的同名函数一样,除了原来的函数使用参数char *,而统一码版本的函数参数为jschar *

操作JS数据类型

JavaScript定义了他自己的数据类型。其中一部分直接对应C中的副本。其他的,诸如JSObject,jsdouble, 和 JSString,对 JavaScript有特殊意义。

一般而言,你在应用程序中声明和使用JS数据类型就和使用标准C数据类型一样。然而,JS引擎对JS数据类型,也就是需要超过一个字空间的变量变量JSObject,jsdouble, 和JSString有不同的跟踪。引擎周期性地检查这些变量,察看他们是否还在使用中。如果不再使用了,就收集他们,释放存储空间来重新使用。

垃圾收集可以有效重复利用堆的资源,但是过分频繁的垃圾收集也会对性能造成影响。你可以根据JS运行时控制垃圾收集的频率,根据你给程序分配的JS运行时的大小和你应用程序使用的JS变量和对象的数量之间的关系。如果你的程序要创建和使用很多JS对象和变量,你可能就要分配足够大的运行时来减少垃圾收集的可能频率。

注意你的应用程序要在任何时候调用同样能JS_GC或者JS_MaybeGC来强制进行垃圾收集。JS_GC将强制进行垃圾收集。JS_MaybeGC则会根据条件进行垃圾收集,如果你调用这个函数时,初始化时分配的空间的特定比例已经被使用的话,就进行垃圾收集。

操作JS值

除了JS数据类型之外,JS引擎也使用JS值,称之为jsval。一个jsval本质上是一个指向任意JS数据类型(除了整型)的一个指针。对于整型,jsval直接包含了他自身的整数值。在其他的情况下,指针还会被编码,添加关于它所指的数据的类型的额外信息。使用可以提高引擎的效率,同时也可以让很多API函数来处理不同类型的数据。

引擎API包含了一组用来测试JS值中的JS数据类型的宏。有:

Besides testing ajsval,你也可以检测他是否属于一个基本JS数据类型 (JSVAL_IS_PRIMITIVE)。基本类型包括未定义(undefined)、空(null)、 布尔(boolean)、数值(numeric)和字符串(string)类型。

你可以测试jsval所指的值是否为NULL(JSVAL_IS_NULL) 或者void(JSVAL_IS_VOID)。

如果jsval指向了一个JS数据类型是JSObject,jsdouble, 或者jsstr,你可以将jsval转换成他的内在的类型,只要相应使用JSVAL_TO_OBJECT,JSVAL_TO_DOUBLEJSVAL_TO_STRING。在某些情况下,你的应用程序或者JS引擎调用要求使用一个特定的数据类型的变量或者参数而非一个jsval时,就很有用了。类似地,你可以使用OBJECT_TO_JSVAL,DOUBLE_TO_JSVAL, 和STRING_TO_JSVAL, 把JSObject,jsdouble, 和jsstr相应地转换成jsval

操作JS字符串

在JavaScript中你的很多工作都回涉及字符串。JS引擎实现了一个JS字符串类型,JSString,一个指向JS字符—jschar—数组的指针,用来处理支持统一码的字符串。引擎也实现了一系列丰富的通用和统一码字符串管理程序。最后,JS引擎提供了对限定字符串的支持,这可以将两个或多个相同的字符串创建时在内存中共享一个单独的实例。对于JSString类型的字符串,引擎会跟踪和管理字符串资源。

通常情况下,当你在处理JS引擎使用的字符串时,你应该使用JS API中的字符串处理函数来创建和复制字符串。还有创建以空字符结尾的和特定长度的字符串的例程,以及获取字符串长度和比较字符串。

统一码字符串支持

使用统一码(Unicode)的API字符串函数的名称和标准的引擎API字符串行数是一一对应的,规则如下:如果标准函数名是JS_NewStringCopyN,相应的统一码版本就是JS_NewUCStringCopyN。同样有针对限定字符串的支持统一码的API字符串函数。

限定字符串支持

为了节省存储空间,JS引擎提供了对共享一个单独的字符串实例支持,这些字符串属于一些独立的不可变化的文字。这种被共享的字符串被称为“限定字符串”(interned string)。当你觉得某个特定的文本会被创建并且反复在程序中使用多次的话,那可以使用限定字符串。

引擎的API提供了几种工作于限定字符串的函数调用:

管理安全性

现在使用JavaScript 1.3,JS引擎加入了安全性增强API函数来编译和执行传送给引擎的脚本和函数。JS安全模型是基于Java的基本安全模型的。该模型提供了一个公共安全接口,但是实际的安全控制由你去实现。

在使用JavaScript的应用中使用安全管理的一个常用的方法是比较脚本的来源和限制脚本的交互。例如,你可能会比较两个或多个脚本的代码源并且只允许来自相同的代码源的脚本修改共享代码源的脚本的属性。

如要实现安全JS,请按照以下几步:

  1. 在代码中声明一个或多个JSPrincipals类型的结构体(struct)。

  2. 把实现了安全信息的函数列表添加到数组中。这些包括了为程序提供原则数组的函数,和使用给定原则的JS对象的引用计数增减机制。

  3. JSPrincipals结构和你的安全信息组装起来。这个信息可以包括一般代码源信息。

  4. 在运行时,使用一些特定的JS API调用来编译和执行所有要应用安全性的脚本和函数,他们将要求传递一个JSPrincipals结构。下面的表格列出了这些API函数和他们的作用:

函数

目的

JS_CompileScriptForPrincipals

编译(但是不执行)一个启用安全控制的脚本。

JS_CompileUCScriptForPrincipals

编译(但不执行)一个启用安全控制、统一码编码的脚本。

JS_CompileFunctionForPrincipals

从一个文本串创建一个启用安全控制的JS函数。

JS_CompileUCFunctionForPrincipals

从一个统一码编码的字符串中创建一个带安全信息的JS函数。

JS_EvaluateScriptForPrincipals

编译和执行一个启用安全控制的脚本。

JS_EvaluateUCScriptForPrincipals

编译并执行一个启用安全控制且用统一码编码的脚本。

JavaScript中的类继承

JavaScript中的类继承

DouglasCrockford
www.crockford.com

翻译 ShiningRay @ www.nirvanastudio.org

And you think you’re so clever and classless and free
–John Lennon

JavaScript一种没有类的,面向对象的语言,它使用原型继承来代替类继承。这个可能对受过传统的面向对象语言(如C++和Java)训练的程序员来说有点迷惑。JavaScript的原型继承比类继承有更强大的表现力,现在就让我们来看看。

Java

JavaScript

强类型

弱类型

静态

动态

基于类

基于原型

函数

构造器

函数

方法

函数

但首先,为什么我们如此关心继承呢?主要有两个原因。第一个是类型有利。我们希望语言系统可以自动进行类似类型引用的转换cast。小类型安全可以从一个要求程序显示地转换对象引用的类型系统中获得。这是强类型语言最关键的要点,但是这对像JavaScript这样的弱类型语言是无关的,JavaScript中的类引用无须强制转换。

第二个原因是为了代码的复用。在程序中常常会发现很多对象都会实现同一些方法。类让建立单一的一个定义集中建立对象成为可能。在对象中包含其他对象也包含的对象也是很常见的,但是区别仅仅是一小部分方法的添加或者修改。类继承对这个十分有用,但原型继承甚至更有用。

要展示这一点,我们要介绍一个小小的“甜点”可以主我们像一个常规的类语言一样写代码。我们然后会展示一些在类语言中没有的有用的模式。最后,我们会就会解释这些“甜点”。

类继承

首先,我们建立一个Parenizor类,它有成员 valuegetset方法,还有一个会将value包装在括号内的toString方法。

这个语法可能没什么用,但它很容易看出其中类的形式。method方法接受一个方法名和一个函数,并把它们放入类中作为公共方法。

现在我们可以写成

正如期望的那样,myString"(0)"

现在我们要建立另一个继承自Parenizor的类,它基本上是一样的除了toString方法将会产生"-0-"如果value是零或者空。

inherits方法类似于Java的extends uber方法类似于Javasuper。它令一个方法调用父类的方法(更改了名称是为了避免和保留字冲突)。

我们可以写成这样

这次, myString"-0-".

JavaScript 并没有类,但我们可以编程达到这个目的。

多继承

通过操作一个函数的prototype对象,我们可以实现多继承。混合多继承难以实现而且可能会遭到名称冲突的危险。我们可以在JavaScript中实现混合多继承,但这个例子我们将使用一个较规范的形式称为瑞士继承SwissInheritance.

假设有一个NumberValue类有一个setValue方法用来检查 value是不是在一个指定范围内的一个数,并在适当的时候抛出异常。我们只要它的setValuesetRange方法给我们的ZParenizor。我们当然不想要它的toString方法。这样,我们写到:

这个将仅仅添加需要的方法。

寄生继承

这是另一个书写 ZParenizor类的方法。并不从 Parenizor继承,而是写了一个调用了Parenizor构造器的构造器,并对结果修改最后返回这个结果。这个构造器添加的是特权方法而非公共方法。

类继承是一种“是……”的关系,而寄生继承是一个关于“原是……而现在是……”的关系。构造器在对象的构造中扮演了大量的角色。注意uber (代替super关键字)对特权方法仍有效。

类扩展

JavaScript的动态性让我们可以对一个已有的类添加或替换方法。我们可以在任何时候调用方法。我们可以随时地扩展一个类。继承不是这个方式。所以我们把这种情况称为“类扩展”来避免和Java的extends──也叫扩展,但不是一回事──相混淆。

对象扩展

在静态面向对象语言中,如果你想要一个对象和另一个对象有所区别,你必须新建立一个类。但在JavaScript中,你可以向单独的对象添加方法而不用新建类。这会有巨大的能量因为你就可以书写尽量少的类,类也可以写得更简单。想想JavaScript的对象就像哈希表一样。你可以在任何时候添加新的值。如果这个值是一个函数,那他就会成为一个方法。

这样在上面的例子中,我完全不需要 ZParenizor类。我只要简单修改一下我的实例就行了。

我们给 myParenizor实例添加了一个 toString方法而没有使用任何继承。我们可以演化单独的实例因为这个语言是无类型的。

小甜点

要让上面的例子运行起来,我写了四个“甜点”方法。首先,method方法,可以把一个实例方法添加到一个类中。

这个将会添加一个公共方法到 Function.prototype中,这样通过类扩展所有的函数都可以用它了。它要一个名称和一个函数作为参数。

它返回 this。当我写一个没有返回值的方法时,我通常都会让它返回this。这样可以形成链式语句。

下面是 inherits方法,它会指出一个类是继承自另一个类的。它必须在两个类都定义完了之后才能定义,但要在方法继承之前调用。

再来,我们扩展 Function类。我们加入一个 parent类的实例并将它做为新的prototype。我们也必须修正constructor字段,同时我们加入uber方法。

uber方法将会在自己的prototype中查找某个方法。这个是寄生继承或类扩展的一种情况。如果我们是类继承,那么我们要找到parentprototype中的函数。return语句调用了函数的apply方法来调用该函数,同时显示地设置this并传递参数。参数(如果有的话)可以从arguments数组中获得。不幸的是,arguments数组并不是一个真正的数组,所以我们又要用到apply来调用数组中的slice方法。

最后,swiss方法

The swiss方法对每个参数进行循环。每个名称,它都将parent的原型中的成员复制下来到新的类的prototype中。

总结

JavaScript可以像类语言那样使用,但它也有一种十分独特的表现层次。我们已经看过了类继承、瑞士继承、寄生继承、类扩展和对象扩展。这一等系列代码复用的模式都能来自这个一直被认为是很小、很简单的JavaScript语言。

类对象属于“硬的”。给一个“硬的”对象添加成员的唯一的方法是建立一个新的类。在JavaScript中,对象是“软的”。要给一个“软”对象添加成员只要简单的赋值就行了。

因为JavaScript中的类是这样地灵活,你可能会还想到更复杂的类继承。但深度继承并不合适。浅继承则较有效而且更易表达。