过眼•物影天狼

博客通天下 淡墨书豪辞

Flower

Archive for the ‘程序设计’ Category

答 zhangdi 同学的问题

      Zhangdi 同学说,评论我的《Java 和 .Net 在异常处理机制上的区别》帖子说:我并不认为 C#(.NET)这样处理 Exception 是好的。我觉得这与 strong-type 的概念相冲突。几个问题:

  1. 在设计一个接口的时候,是不是需要同时告诉用户,这个接口有可能抛出什么类型的 Exception?

    答:这种说法自然是没错的,异常应该是接口设计的一部分,但是告知用户的方式可以有很多种。Java 的这种方式只是其中较为有效的方式之一,他通过编译器做出严格的限制,这其中兼有优劣。

  2. 如果需要,那么这个 Exception 类型是否应该成为一个接口声明的一部分?

    答:其实这还要从程序整体的设计角度来讨论。在 Java 中非 RuntimeException 其实已经不是一种普通的异常那么简单。因为最终在程序运行的时候,Java 不允许有非 RuntimeException 被抛出到虚拟机,这也就是说这种异常看上去只存在于设计时。他更像是程序运行期间的一种消息传递机制,只是首先这种消息“有别于(Except)”其他的返回类型,另外就是它会沿着调用栈强行向上追溯。显然这种异常更多的是在程序设计阶段起到操控程序非正常运行时行为的作用。因此他更像是一种接口,自然声明这样的接口也是理所应当的。

          但是 Java 还有 RuntimeException 的概念,而 .Net 或许就是没有区分他们而已,因为一旦程序遇到异常,无论是被程序本身恢复,还是被虚拟机捕捉到导致强行非法结束,都是可以接受的,毕竟对用户而言除了造成不方便,并不会有安全上的隐患。

  3. 一个已知的有可能发生的 Exception 被操作系统捕获是否可以说是用户在处理这个接口的声明的时候,出现了疏忽呢?

    答:这个自然,如果不是 Java 概念中的 RuntimeException,那么如果真有这样的异常被虚拟机捕捉到肯定就是 Bug,但至少是一个安全的 Bug。而同时,就像我在帖子里说到的,需要在开发难度和设计完整性上达到一定的平衡。.Net 这样自然忽视了后者,但同时显然使用就方便多了。

  4. strong-type 的目标是在编译器发现尽可能多的关于类型的错误,如果我们把接口的定义看作类型的一部分,那么是否可以说,这个有可能将 checked exception 暴露给操作系统的编译器在 strong-type 上有缺陷呢?

    答:没看懂!:-)

      还是那句话,我觉得我写这篇帖子已经足够公正了。.Net 在异常处理的设计上显然不如 Java 更趋近完美,但毕竟还是可以用的,还是好用的,剩下的瑕疵是可以通过程序员的经验,以及良好的开发习惯和管理来弥补的。

Java 和 .Net 在异常处理机制上的区别

      关于 Java 和 .Net 优劣的争论一直在继续,而在异常处理方面体现得最为激烈,因为他们之间的差异是如此明显。.Net 晚于 Java 出现,那么 Java 对 .Net 就理应起到很重要的借鉴作用,但是伟大的 Anders Hejlsberg 为什么没有继续 Java 的实现方式,而是另辟蹊径,这是一个非常值得研究的问题。因为我们要承认一个真理:正确的东西大家都是一样的正确,错误的却各有个的错误。可以肯定这不可能是 Anders 的疏忽,那么他的道理究竟何在,或者说他们之间究竟有什么区别?

      在你能耐下心来看完这篇帖子之前,我想要明确告诉你一个结论:Java 和 .Net 在异常处理的本质上是没有区别的。

一、Java 是如何处理异常的

      如果一个 Java 方法要抛出异常,那么需要在这个方法后面用 throws 关键字定义可以抛出的异常类型。倘若没有定义,就认为该方法不抛出任何异常。如果从方法的入口和出口的角度去考虑一下这个规范,我们知道参数可以认为是方法的入口(当然某些情况下也可以是出口),而返回值则是方法的出口,这是在程序正常执行的情况下,数据从入口入,出口出。要是程序非正常执行,产生异常又当如何? 被抛出的异常应该如何从方法中释放出来呢? Java 的这种语法规范就如同给异常开了一个后门,让异常可以堂而皇之“正确”地从方法里被抛出。

      这样的规范决定了 Java 语法必须强行对异常进行 try-catch。设想一下,对于以下的方法签名:

public void foo() throws BarException { ... }

暗含了两方面的意思:第一,该方法要抛出 BarException 类型的异常;第二,除了 BarException 外不能抛出其他的异常。而正是这第二点的缘故,我们要如何保证没有除 BarException 之外的任何异常被抛出呢? 很显然,就需要 try-catch 其他的异常。也就是说,一般情况下,方法不抛出哪些异常就要在方法内部 try-catch 这些异常。

      Java 这样的机制既有优点,也有缺点。先来说说优点:

  • 很显然,这种规范是由 Java 编译器决定的。倘若 Java 程序的入口点 main() 方法没有任何异常抛出,就是说要在 main() 方法内部,即整个程序内部捕捉所有的异常,否则将无法通过编译。这样编译器保证了程序对每个异常都有相应的计划和处理,不会有未处理的异常被泄露到虚拟机中,导致程序意外中断或退出,也就是增强了程序的健壮性。当然,Java 有 RuntimeException 的概念,这样的异常仍然可以随时被抛出到虚拟机中。
  • 强行 try-catch 要求把异常作为程序设计的一部分看待。就如同方法的参数和返回值一样,在编写一个方法时,要结合上下文做出通盘的考虑和打算。虽然异常是所谓的“意外情况”,但是这些“例外”理应是被我们全部了解并处理的。
  • 方便调试。异常理应在正确的位置被捕捉。当异常发生时,我们能更清楚的了解到其来源和相应处理程序的位置,而免去了在整个调用栈中摸索的麻烦。
  • 在不借助任何文档的情况下,从方法签名就可以知晓应该对哪些异常进行处理。

      Java 异常处理机制的这些优点也直接导致了他的致命弱点:将程序变得异常繁复。往往一个简单的程序,功能代码寥寥几行,而异常处理部分却占用了程序的绝大部分篇幅;同时导致缩进深度加深,既不利于书写,也不利于阅读。另外他的强行 try-catch 需要程序员有更高深的造诣,能够通盘考虑异常处理设计问题,这个在程序开始之初或者对于初学者是一个不小的门槛。这往往会阻碍其推广与发展,因为低水平初学者的信心往往因此而受到打击。然而对于高手来说,编译器是否能帮助他们找到未被处理的异常只是一个方便与否的问题,只要在编写方法时注意了异常处理,即便没有编译器的支持,情况也不会糟糕太多。反而倒是由于要遵循这样复杂的异常处理规范,以至于大多数人都可能为了图一时方便,对异常的基类型 Exception 或 Throwable 进行笼统地捕捉,这样做的危害就是那些你无法处理的异常被溺死在处理程序中,(按照异常处理原则,我们应该只捕捉那些可以被处理或恢复的异常,而把其他的异常继续抛出。至于这样做的优势,以及不这样做所带来的问题,不是一两句能够说清楚,这里就不展开讨论了。)导致程序的不稳定和不确定。既没有发挥 Java 语法在这方面的优势,反而增加了忧患。

二、.Net 是如何处理异常的

      一句话概括 .Net 的异常处理方式就是随心所欲。没有人要求你一定要抛出异常,也更没有人要求你一定要捕捉异常。未被捕捉的异常会被以 Unhandled Exception 的形式抛出到虚拟机中。在此我就要先解决一下文章开头提到的问题,为什么说 Java 和 .Net 这两种异常处理机制在本质上是相同的。可以从两个方面来考虑:

  1. 默认情况下。Java 在默认情况下 main() 方法是不抛出异常的,正如前面所说的,这要求所有的异常都必须在 main() 方法内部被捕捉;而 .Net 则没有这种约束,他的 Main() 以至于整个应用程序中的任何一个方法对异常都是完全开放的。这样来看,这两者刚好是对立互补的。
  2. 非默认情况下。Java 可以通过在 main() 方法后面加 throws 关键字使得整个应用程序对异常开放;而 .Net 则可以通过给应用程序域(Application Domain)的 UnhandledException 事件添加委托达到捕捉所有异常的目的,很显然这又是对立互补的。

因此,就好像一个是“正反”,一个是“反正”,加在一起“正反反正”都是一样的,对于达到控制异常的目录来说,是没有区别的。

      很多 Java 爱好者都鄙视 .Net 的这种行为,一方面他令程序变得不够健壮,因为默认情况下没有强制的办法要求所有的异常都被处理,或被正确处理;另外,他为调试增加了困难,不借助文档或代码你将无法了解到一个方法可能抛出什么异常,而当一个异常被抛出的时候,同时异常处理代码又写得不够完善,你将不得不仔细查看整个调用栈来确定异常出现的位置,而对于这一点 Java 默认是强制的。

      但是 Anders 的想法总是有道理的。

  1. .Net 代码写起来非常容易。这是对于初学者,或者那些只是想实现一些测试性小功能的人而言,你完全没有必要考虑太多异常处理的细节,你要的就是写代码,然后让他跑起来。这样的简单性无疑是你希望看到的,这样的简单性无疑更有利于 .Net 在市场上的推广。由于他在这方面并没有什么理论上的漏洞,也就仍然适合构建庞大的项目,只是感觉没有那么舒服罢了。
  2. 一定程度上增加了程序的安全性。难道不捕捉异常可以被成为是安全的吗?这个话也许要从另外一方面来想,前面说过,有些 Java 程序员(绝对不占少数)为了图省事,在强行捕捉异常的压迫下,选择捕捉异常的基类型,也就是捕捉所有的异常。这样当有你无法处理的异常出现时,他们就溺死在了你的代码中,而外面的程序全然不知,还在以一种不确定的状态运行着,这就可能是危险的开始。而如果是 .Net,那么 Unhandled Exception 会被虚拟机捕获,导致程序异常退出,虽然这从面子上对于用户不是一个好的交代,但是深层次地他避免了程序在危险的状态下继续运行。

      总之,萝卜白菜各有所爱。我的这篇帖子力求公正地讨论了这个问题,希望能对你有所帮助。

测试驱动开发和软件开发流程模型

      我终于决定不再继续看极限编程大师 Kent Beck 所写的那本 Test Driven Development: By Example 了。因为我实在无法忍受我与作者在软件开发流程模型上的分歧。但这并不妨碍我认同并实践作者所谓“测试先行”的理念。我是一个趋近完美主义者,因此我更喜欢像设计模式那样偏重设计的方法论,我不能认可一个靠不断修改而成长起来的软件。

      按照 Kent Beck 极端的测试驱动开发模型,一个软件应该是这样写出来的:

  1. 写测试代码。
  2. 编译测试代码,编译无法通过。(因为没有实现代码。)
  3. 写一份最简单的实现代码,让编译通过。
  4. 迭代地修改代码以减少实现代码的重复度,降低实现代码和测试代码之间的耦合度。

就是这样,一个软件被一点一点修改而成。他强调了在这个过程中有两个主意事项:第一,在添加任何功能之前都要先写测试,除非代码是用于调试的,这也就是所谓测试先行的概念;第二,当编译无法通过时尽量不要再写新的测试。

      我之所以不认同这个模型,就是因为其中间产生了大量的迭代过程,而这些过程很可能只在头脑中出现,或者根本就不曾出现。这些过程一方面增加了开发的消耗,也在某种程度上引入了更多的不确定性。但是其中测试先行的做法,能最大程度保证单元测试的完整性与正确性。

      因此,我理想中的开发流程模型应该是这样的:

  1. 用户需求调研,导出成软件需求
  2. 根据软件需求进行设计
  3. 根据设计编写测试用例
  4. 根据测试用例编写测试代码
  5. 构建软件框架使测试代码能够编译通过
  6. 写 Mock 来保证测试用例代码准确无误
  7. 按部就班地写程序,用测试用例来保证所写的代码准确无误

这其中可以按照极限编程的思想以一个可独立发布可运行的模块作为单位,这样可以有效地控制模块的大小,还可以避免出现过度设计的问题。

在 Sourceforge 项目空间上部署 MediaWiki(2)

      《在 Sourceforge 项目空间上部署 MediaWiki》的第一部分在这里

      造成 MediaWiki 不能向 config 目录写文件的原因,估计是 Sourceforge 服务器没有开放这样的权限。幸亏,MediaWiki 提供了一个变通的办法。找到下面的这些代码,把他们注释掉:

if( !is_writable( "." ) ) {
    dieout( "<h2>;Can't write config file, aborting</h2>

    <p>In order to configure the wiki you have to make the
    <tt>config</tt> subdirectory writable by the web server. Once configuration is done you'll move the created
    <tt>LocalSettings.php</tt> to the parent directory, and for added safety you can then remove the
    <tt>config</tt> subdirectory entirely.</p>

    <p>To make the directory writable on a Unix/Linux system:</p>

    <pre>
    cd <i>/path/to/wiki</i>
    chmod a+w config
    </pre> );
}

      这样一来,当目录没有写权限的时候,操作不会被取消,而是生成的配置文件会以文本的形式被打印在页面上。把这些代码拷贝、粘贴到一个名为 LocalSettings.php 的文件,然后上传到 MediaWiki 的根目录,在我们这个例子中也就是 /home/groups/s/sa/sample/htdocs/wiki/ 就可以了。

      最后还需要解决一下 session 的问题:

      由于 Sourceforge 使用了服务器集群,所以把 session 保存在内存中是有问题的,所以需要用文件来存储 session:

  1. 在 /tmp/persistent/ 下面创建一个用来保存 session 的目录,例如 /tmp/persistent/sample/sessions。这里的 sample 就是你的项目的 unix name。因为这个名称是唯一的,所以不用担心会跟别人的重复。但注意千万不要把它建在你的 project 目录下,因为他们是不能被 web 服务器写入的。
  2. 确保这个目录是可写的,也就是执行一下 chmod a+w /tmp/persistent/sample/sessions。sample 仍然是项目的 unix name。
  3. 告诉 MediaWiki 使用这个目录来存放 session,方法是在 LocalSettings.php 文件的开始处添加 session_save_path("/tmp/persistent/sample/sessions/"); 注意不要丢掉 sessions 后面的那个 "/"。

      如果你这样做之后,在编辑或查看页面的时候退出,碰到空白页面,请把 ini_set( "include_path", ".:$IP:$IP/includes:$IP/languages" ); 添加在上一步的 session_save_path 之前。

      这样,你的 Wiki 就基本上可以在 Sourceforge 的项目空间上运行了,不过还有很多问题这里没有涉及到,比如:如何配置邮件系统、如何修改网站的 logo、或者是一些碰到的其他的问题。更多内容请详见 Running MediaWiki on Sourceforge.net。(中国大陆地区访问可能会有障碍。)

在 Sourceforge 项目空间上部署 MediaWiki

      在开始部署之前,有一些准备工作要做:

  1. 你需要在 Sourceforge 上面拥有自己的帐号,并加入到一个项目,而且你的帐号在该项目中需要有访问 shell 的权限。同时确保该项目已经开通 MySql 数据库。
  2. 到 MediaWiki 的官方网站去下载 MediaWiki 1.6.8 版本。注意千万不要下载最新版本,因为 MediaWiki 从 1.7 版以后需要 php 5 的支持,而 sourceforge 服务器上部署的仍然是 php 4,因此无法运行最新版本。
  3. PuTTY 网站上下载 PuTTY.exePSFTP.exe

      为了方便,假设我在 Sourceforge 上拥有用户 usr,密码为 pwd,而且参与的项目为 sample。该项目的 MySql 数据库具有读写权限,可以添加、删除表和索引,并可以锁定表的用户为 mysqlusr,密码 mysqlpwd。

把下载来的 mediawiki-1.6.8.tar.gz 文件上传到项目空间,具体做法如下:

  1. 运行 psftp.exe
  2. 执行 open shell.sourceforge.net
  3. 按照提示输入你在 sourceforge 上注册的用户名,例如 usr
  4. 输入用户密码,例如 pwd
  5. 进入项目的 htdocs 目录,执行(以 sample 项目为例) cd /home/groups/s/sa/sample/htdocs/
  6. 把 mediawiki-1.6.8.tar.gz 复制到 psftp.exe 所在目录
  7. 运行 put mediawiki-1.6.8.tar.gz
  8. 待上传完成,关闭 psftp

解压已经上传的 mediawiki-1.6.8.tar.gz 文件:

  1. 运行 putty.exe
  2. 在 PuTTY Configuration 界面的 Host Name 中输入 shell.sourceforge.net,确认 Protocal 选中 SSH,然后点 Open
  3. 按照提示输入用户名,例如 usr
  4. 然后输入用户密码,例如 pwd
  5. 进入项目的 htdocs 目录,执行(以 sample 项目为例) cd /home/groups/s/sa/sample/htdocs/
  6. 执行 tar -xvzf mediawiki-1.6.8.tar.gz
  7. 将解压后的目录重命名为 wiki,执行 mv mediawiki-1.6.8 wiki

      登录 phpMyAdmin,创建一个 Database,例如 wikidb。当然用 sourceforge 提供的 MySql 服务创建 Database 时,命名都需要有一个前缀,在此忽略。

      打开浏览器,登录项目 Wiki,例如 http://sample.sourceforge.net/wiki。此时由于 MediaWiki 尚未配置,信息提示让你进入配置页面,点击进入配置页面,发现出错了。大意是说你的 config 目录没有写权限,让你在 MediaWiki 的安装目录执行 chmod a+w config 命令更改权限。但是,尽管你按照提示完成了设置,错误依旧。

      本文的第二部分在这里

不要跟 prototype 一起学坏

      新版本的 JSON 修改了 API,将 JSON.stringify() 和 JSON.parse() 两个方法都注入到了 Javascript 的内建对象里面,前者变成了 Object.toJSONString(),而后者变成了 String.parseJSON()。

      不可否认,经过这样的修改,JSON 的接口确实比以前漂亮且好用多了,对于这样的对象:

var obj = new Object;
var arr = new Array();
arr[0] = "a";
arr[1] = "b";
obj["key1"] = arr;
obj["key2"] = "c";

以前的代码得这样写:

var jsonString = JSON.stringify(obj);
var myObj = JSON.parse(jsonString);

而现在的代码是:

var jsonString = obj.toJSONString();
var myObj = jsonString.parseJSON();

      前者 JSON 作为一个工具类,提供一组方法实现转换功能,而后者让你觉得对象天生就具有这样的方法。如果对于编译语言来说,这可能没什么问题,但是对于 Javascript 问题就来了。考虑下面的代码片段:

var obj = new Object(); 
for (var k in obj) {
    document.write(k + "<br>");
}

      如果是一个干净(没有包含任何其他 Javascript 类库)的 Javascript 执行环境,这段代码应该不会输出任何内容,因为 obj 是空的;但是当代码中包含了新版本的 JSON 后,你会发现结果是 toJSONString。很多情况下我们会用 Javascript 的 Object 来模拟 Map,那么上面的这段代码的功能就是遍历此 Map,一个空的 Map 怎么会凭空出来了一个成员?这正是由于新版本的 JSON 的实现所决定的:

Object.prototype.toJSONString = function () {}

      JSON 并不是第一个采用这种写法的 Javascript 类库,始作俑者就是我文章标题中提到的 prototype。prototype 为 Object 注入了 extend 和 instanceof 等方法来支持 Javascript 的面向对象扩展,这样写的后果就是造成了非常严重的兼容性问题。你有没有注意到为什么有些 Javascript 的类库特别声明"使用了 prototype"或者说是"与 prototype 兼容"?因为如果他使用了 prototype 那么你需要在用此类库的时候格外小心,像上面那样的代码不能正常工作;而如果说其与 prototype 兼容,那么可能该类库并没有使用 prototype 但是你使用 prototype 并不会影响他的正常运行。

      prototype 固然是个成功且应用广泛的 Javascript 框架,但是这并不能说明他解决问题的思路就是正确的。开发编译语言程序的人们都知道要以组织或公司的名称作为命名空间前缀,来避免冲突,奈何如此著名的两大 Javascript 框架提供方反而积极去打破这个规则呢?难道实现了功能就可以不考虑兼容性的问题吗?

如何重写 Equals 方法

      不要以为这是件容易的事情,先来看一个正确的 Equals 方法应该具备的条件:

  1. obj.Equals(obj) 应该永远返回真
  2. obj1.Equals(obj2) 和 obj2.Equals(obj1) 应该返回相同的结果
  3. 如果 obj1.Equals(obj2) 而 obj2.Equals(obj3) 那么 obj1.Equals(obj3) 应该也为真

别以为我的智商就小学水平,回去看看你的代码,这些都能做到吗?写一个具备如此条件的 Equals 方法不是件容易的事情!从实现角度说,一般来说分为两种情况:

一、给直接继承自 Object 基类的类型重写 Equals 方法

public class MyType {
    private int x;
    private string s;

    public override bool Equals(object obj) {
        // 如果 obj 是空的话,就直接返回假
        // 因为当前对象不会是空,否则在还没有调用 Equals 方法之前就会先抛出空指针异常
        if (obj == null) return false;

        // 如果 obj 与当前对象不属于同一个类型,也直接返回假
        // 显然两个不同类型的对象不可能相等
        if (this.getType() != obj.getType()) return false;

        // 这个强制类型转换是不会抛出异常的
        // 因为你已经知道 obj 属于该类型
        MyType o = (MyType)obj; 

        // 最后逐个比较他们的成员变量是否相等就可以了
        if (this.x != o.x || this.s != o.s) return false;

        return true;
    }
}

二、给间接继承自 Object 基类的类型重写 Equals 方法

// MyType2 继承自 MyType,而不是直接继承自 Object
public class MyType2 : MyType {
    private DateTime d;
    
    public override bool Equals(object obj) {
        // 如果基类认为这两个对象不相等
        // 直接返回假
        if (!base.Equals(obj)) return false;

        // 两种实现方法的区别仅在于此,下面都是一样的
        // 直接继承自 Object 的类型之所以没有上面这一段
        // 是因为 Object 判断两个对象是否相等的逻辑非常简单
        // 通常就是判断他们的引用是否相等。因为当前对象和 obj 很有可能是不同的引用
        // 所以这种情况下,你的 Equals 方法绝大多数情况下都会返回假
        // 如果 obj 是空的话,就直接返回假
        // 因为当前对象不会是空,否则在还没有调用 Equals 方法之前就会先抛出空指针异常
        if (obj == null) return false;

        // 如果 obj 与当前对象不属于同一个类型,也直接返回假
        // 显然两个不同类型的对象不可能相等
        if (this.getType() != obj.getType()) return false;

        // 这个强制类型转换是不会抛出异常的
        // 因为你已经知道 obj 属于该类型
        MyType o = (MyClass)obj;

        // 最后逐个比较他们的成员变量是否相等就可以
        if (this.d != o.d) return false;

        return true;
    }
}

参考了 Jeffry Richter 的 Applied Microsoft.NET Framework Programming 一书。

给 dasBlog 添加支持 SiteMap 的功能

      今天享受了一下 Google 的 WebMasters 服务,再加上 Google Analytics,真有 WebMater 的感觉。

      在这个服务里面,为了网络爬虫能够更好地抓取页面,Google 需要你提供 SiteMap,一个 XML 格式文件。试了 Google 推荐的几个程序和服务,感觉都不是很好,鉴于 dasBlog 是开源的,简单研究了一下,为其添加了生成 SiteMap 的功能。

      首先,下载 net.tangrui.DasBlog.Web.Services.dll 文件,将其放到 dasBlog 的 bin 目录下。

      然后,修改 web.config 文件,在 <httpHandlers> 标签下,添加这样的节点:

<add verb="*" 
  path="sitemap.ashx" 
  type="net.tangrui.DasBlog.Web.Services.SiteMapHandler, net.tangrui.DasBlog.Web.Services" />

      最后,访问 http://yourwebsite/sitemap.ashx 就可以了。

      如果需要,可以到这里下载源代码。

一个好的错误提示信息应该是什么样子的

礼貌:让用户感觉像是一个白痴是非常不好的。

可读性:粗劣的语法和易曲解的句子结构是不好的。

正确性:错误信息必须正确描述问题所在。

精确性:“什么东西错了”是个正确的错误信息,但不非常精确。

诊断而不是指示:描述问题而不是解决方法。

可翻译性:可以被容易地翻译成其他语言。

window.eval() 和 eval() 的却别?

      前两天作了一个试验,把 Javascript 文件当作一个字符串读到浏览器的内存中,然后使用 eval() 方法来解析,对某些内容的 Javascript 文件(我用的是 prototype.js)会有异常抛出。好像浏览器的 Javascript 引擎总是试图去执行一些本来是声名性的语句,例如定义 function。在我的试验中,当解析到 prototype 给 Object 添加的 extend 方法时,说第一个参数 destination 没有定义。

      当研究了另一个非常有名的 Ajax 框架 Dojo 之后发现,如果使用 window.eval() 方法,上面的问题就没有了,解析可以非常顺利地进行,就像把 Javascript 文件通过 <script> 标签引用近来一样。这就产生了一个问题难道 window.eval() 和 eval() 还不一样?

      众所周知,根据 Javascript 规范,其实那些我们经常用到的理解意义上的全局方法,比如 alert(),其实也是有它的依存对象的,这个对象就是 html dom 的 window,也就是说 alert() 就等于 window.alert(),当然 eval() 应该也不例外,但是为什么在这里表现却是如此的不同?

      上网 google 了一下,在这篇文章中找到一点线索,但是仍然不能解决我的疑问。

      文章中说,本来 Javascript(其实他说的是 JScript)应该有两个方法,一个是 eval() 另一个是 execScript()(但是后来在 w3shools 上面却发现 window 只有 execScript() 方法,却没有 eval()!),前者是在函数调用的上下文中执行,就是说如果你在 function myFun() {} 中使用 eval() 那么它就只能访问 myFun 中的成员;而后者是在全局的上下文中执行。文章后面还说到了一些 firefox 相关的问题,但是我发现 ie 的行为在这一点上跟 firefox 是一样的。如果这两个方法仅仅有这点不同的话,那么真的可以像文章中说得那样理解:

  • 如果在函数中使用 window.eval() 来执行,则使用全局上下文环境。
  • 如果使用 eval() 来执行,则使用当前函数的上下文环境。

但是为什么使用函数上下文环境解析就会试图去执行声名性语句呢?