65.9K
CodeProject 正在变化。 阅读更多。
Home

MiniRacer - 使用 JavaScript 扩展 W3C DOM 元素 (IE)

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.13/5 (5投票s)

2005年2月16日

8分钟阅读

viewsIcon

84410

downloadIcon

499

一个基于表格的 JavaScript 驾驶游戏,演示如何通过添加自己的功能来扩展 W3C DOM 元素。

MiniRacer Screenshot

引言

这款简短的 JavaScript 游戏背后的代码面向中级水平,旨在演示有用的技术:

  • 以面向对象的方式编写 JavaScript
  • 将该技术应用于扩展 W3C DOM HTMLElement(在此情况下为 table、table row 和 table cell 元素)
  • 从而创建我们的“类”(严格来说是函数)的实例,这些实例同时也是 HTMLElement 的实例,因此可以直接添加到我们的网页中,同时仍然展现我们添加的功能。

因此,该游戏面向 Internet Explorer 6(及兼容版本)浏览器。为了简单起见(以及为了省事),我没有涉足浏览器兼容性的复杂领域,但支持 W3C DOM 的一个重要原因是希望随着时间的推移,跨浏览器的代码将越来越标准化。

本文首先介绍游戏以及继承(JavaScript 中的原型)的基本原理。有关扩展 HTMLElement 的具体细节,请跳至本文末尾。

我需要说明的是,虽然希望这款游戏能成为一些有用技术的良好示例,但它们并非我独创的技术。有些是众所周知的;有些则不太为人所知。本文底部的“参考文献”部分将读者引向了在我编写这款游戏时最有用的四篇参考文献。

游戏

这款游戏是我多年前在 Amstrad CPC464 上玩过的简单驾驶游戏。它利用一个表格来表示赛道,表格单元格构成赛道的方块。四个按钮允许您在四个方向上操纵汽车。目标是在不撞墙的情况下到达终点(用红色标示)。

您还可以设计一条新赛道来比赛,更改当前赛道的设计,甚至通过将 URL 添加到您的收藏夹来保存设计好的赛道(赛道存储在查询字符串中)。

您可以 在线 玩这款游戏,或下载包含所有代码的 ZIP 文件。

设计

当然,我们可以使用一系列全局函数来编写类似这样的游戏,这些函数会测试各种条件,并通过 document.write 和修改元素的 innerHTML 属性来向文档写入 HTML。

然而,更面向对象的方法带来的好处是巨大的。特别是:

  • JavaScript 中的调试臭名昭著地混乱。大量的全局函数和随意将原始 HTML 喷入文档是代码混乱的根源——难以理解,调试困难,维护起来更是噩梦。
  • 相反,类(由 JavaScript 函数表示)易于(或更易于)进行单元测试,并且有助于保持代码的整洁和模块化。
  • 随着 W3C DOM(在 IE 6 中得到了相当程度的支持)的日益普及,我们可以基于这些元素来创建对象,这些对象共享其基本功能,但已扩展以满足我们程序的需求。通过利用这些,我认为我们支持了 W3C 在标准化跨浏览器文档对象模型方面所做的重要工作。

由于 JavaScript 不会强制执行任何一种 OO 结构到您的代码中,并且它是一种弱类型语言,因此您需要通过严格的自律来开始掌握这项技术。

下面展示的基本原理需要仔细研究才能掌握,但一旦掌握,它们将带来丰厚的回报。尽管它一开始可能看起来很复杂,但也很容易退后一步,欣赏面向对象解决方案的基本简洁性。该解决方案的 UML 图如下所示。

UML 图

UML Diagram

“继承”W3C DOM 元素的优点是双重的:

  • 首先,我们可以利用它们(有用的)方法。例如,HTMLTableElementinsertRow 方法处理了在任何索引位置向我们的表格添加新空行的所有麻烦。
  • 其次,我们可以将我们的对象实例直接添加到网页中。因为我们的对象也是 HTMLElement 的实例,所以它可以被添加到网页中,使用任何其他 HTMLElementappendChildreplaceChild 方法。

设计原则

我们可以使用以下通用原则来实现这种方法:

  1. 与任何面向对象的方法一样,决定程序所需的类。这次,我使用了:
    • RaceTrack - 用于表示游戏进行的赛道。
    • RaceRow - 用于表示赛道上的一行单元格。
    • RaceCell - 用于表示赛道上的单个单元格。
    • Car - 用于处理汽车的当前位置,以及处理移动更新、转向和检查碰撞点及终点。
    • QueryStringParser - 用于解析传递给 HTML 页面的查询字符串。
    • SpeedSelector - 包装 select 元素以设置汽车的速度。
  2. 为您的每个类创建一个 *js* 文件,例如 *RaceTrack.js*。每个类 *js* 文件都按以下格式设置:
    • 首先是构造函数,其名称与类名相同,例如:RaceTrack
      function RaceTrack()
      {
        this.init();
      }
    • 构造函数简单地调用一个初始化函数:
      this.init();

      init 函数完成对象的全部设置,通常只是初始化成员变量,例如:this._rows = 0;

      [调用 init 函数而不是将初始化代码放入构造函数本身是有原因的。不详细说明,这是为了确保如果您稍后对该函数进行子类化,那么子类的每个对象实例仍然可以获得自己的成员变量,而不是在函数的所有实例之间共享变量。]

    • 构造函数之后是一系列原型语句,它们将成员函数附加到您的类,并给出其实现的函数名称,例如:
      RaceTrack.prototype.init = RaceTrack_init;
      RaceTrack.prototype.clearTrack = RaceTrack_clearTrack;

      请注意,在这些赋值中没有任何参数括号!!

    • 之后是成员函数的实现代码……每个原型语句一个,例如:
      function RaceTrack_init()
      {
        this._rows = 0;
        this._cols = 0;
      }
      
      function RaceTrack_clearTrack()
      {
        …
      }
      
      …
  3. 为您的每个网页创建一个另一个 *js* 文件,以包含该页面的静态代码。例如,在此项目中,我有两个网页:*miniRacer.htm* 和 *designTrack.htm*,因此我创建了两个匹配的 *js* 文件:*miniRacer.js* 和 *designTrack.js*,其中包含该页面的静态函数。

    例如,*miniRacer.js* 包含以下全局函数和变量:

    var tbl;    // stores the race track table
    var car;    // stores as instance of the Car class
    
    // The default track to race on
    var DefaultRaceTrack = "wwwwwwwwwwwwrrr...";
    var DefaultRowLength = 11;
    
    function setup()
    {
      // Load racetrack in from querystring,
      // using default track if not available
      var qs = new QuerystringParser();
      var track = qs.getRaceTrack(DefaultRaceTrack);
      var rowLength = qs.getRowLength(DefaultRowLength);
    
      tbl = document.createElement("RaceTrack");
      tbl.load(track, rowLength, DISABLE_CELL_CLICK_HANDLER);
    
      // add racetrack to webpage
      var RaceTrackContainer = document.getElementById("RaceTrackContainer");
      RaceTrackContainer.replaceChild(tbl, RaceTrackContainer.firstChild);
    
      // create a new instance of our racecar
      car = new Car(tbl, "car", new SpeedSelector("speedSelect"));
    }
    
    function designTrack()
    {
      // called when "Design This Track" link
      // is clicked...redirects to designTrack.htm
      car.reset();
      window.location = "designTrack.htm?track=" + 
         tbl.serialise() + "&rowLength=" + tbl.getRowLength();
    }

    请注意全局函数中的代码量很少。在理想情况下,它们几乎只做创建所需的类实例,然后根据需要将它们连接起来。本质上,所有代码都已委托给最合适的类中最合适的方法。然后,当出现问题时,可以相对容易地对每个方法进行单元测试,从而实现更轻松的测试和调试。

  4. 创建一个 *constants.js* 文件来存储代码常量。例如:
    var ENABLE_CELL_CLICK_HANDLER = true;
    var DISABLE_CELL_CLICK_HANDLER = false;
    var IGNORE_QUERYSTRING = true;

    [或者,您可以将常量附加到其最接近的关联函数,但我还没有走到那一步。]

  5. 最后,我们需要创建一个 *DOM_override.js* 文件来在 W3C DOM 提供的 document.createElement 方法中实现所需的覆盖。此处下方给出了这种需求及其解释。

继承 DOM 元素

这里有一个小陷阱。通常,要利用 JavaScript 中基于函数的等效继承,我们只需将函数的原型对象设置为我们想要继承的函数:

Dog.prototype = new Animal();

然而,在 Internet Explorer 中,DOM 元素不是使用完整的 JavaScript 函数实现的。这在使用原型技术时会导致错误消息。

不过,并非一切都完了。相反,我们在 *DOM_override.js* 中做了两件事:

  • 我们将 document.createElement 函数的默认实现替换为我们自己的实现。
    // store a reference to the original document.createElement
    var __IEcreateElement = document.createElement;
    
    // and now re-define the original
    document.createElement = function (tagName) {
    
      if(tagName=='RaceTrack')
      {
        return document.applyInherit(__IEcreateElement("table"), new RaceTrack());
      } else
      {
        return __IEcreateElement(tagName);
      }
    }

    我们的替换函数通常会委托给原始函数。但是,如果它发现了我们的类名之一(RaceTrack),它会调用 applyInherit,传递一个正确的 HTMLElement 的新实例和一个我们的类(RaceTrack)的新实例。

  • 那么 applyInherit 是做什么的呢?嗯,我们下面定义它:
    document.applyInherit = function(original, interface)
    {
      for (method in interface)
        original[method] = interface[method];
    
      return original;
    }

它只是利用了 JavaScript 函数的所有属性和方法都可以通过索引表示法 [] 来访问这一事实。

我们遍历我们类的所有属性和方法,并将它们复制到 HTMLElement 的实例中。然后我们返回 HTMLElement 的实例,该实例现在已经配备了我们类的所有属性和方法。这实现了继承,尽管是以一种相当非正统和手动的方式。

我们现在可以创建我们类的实例了。我们使用以下方法创建一个我们顶层类 RaceTrack 的实例:

var tbl = document.createElement("RaceTrack");

为了能够创建 RaceRowRaceCell 的实例,我在 RaceTrackRaceRow 中分别添加了工厂方法:

function RaceTrack_insertRaceRow()
{
  return document.applyInherit(this.insertRow(), new RaceRow());
}

function RaceRow_insertRaceCell()
{
  return document.applyInherit(this.insertCell(), new RaceCell());
}

HTMLTableElementHTMLRowElement 分别为我们定义了 this.insertRowthis.insertCell

这些巧妙的函数现在使我们能够使用以下示例代码将 RaceCell 添加到赛道中:

for(var i=0;i<rows;i++)
{
  var row = this.insertRaceRow();
  for(var j=0; j<cols;j++)
  {
    var cell = row.insertRaceCell();
    cell.setX(j);
    cell.setY(i);
  }
}

RaceTrack 的实例(在下面由 tbl 引用)可以使用以下方法插入到网页中:

var RaceTrackContainer = document.getElementById("RaceTrackContainer");
RaceTrackContainer.replaceChild(tbl, RaceTrackContainer.firstChild);

为了让这奏效,我们需要网页中一个 id 为“RaceTrackContainer”的元素。

<td id="RaceTrackContainer">
  Racetrack goes here
</td>

参考文献

我希望本文对您有所帮助。下面给出了一些参考文献,它们都对构建本文有所帮助。

  1. 模拟 Internet Explorer 中 DOM 对象的原型.

    这篇网页介绍了使用 IE 和 JavaScript 扩展 DOM 元素。它还展示了一个使用行为添加功能的巧妙技巧。这里展示的技术基本上就是这个游戏所使用的想法。

  2. JavaScript 对象 [Nakhimovsky & Myers,Wrox Press,ISBN 1861001894]

    一本介绍了 JavaScript 面向对象方法的精彩书籍。我从这本书中借鉴了一些想法来制作这款游戏。

  3. 客户端访问查询字符串.

    使用 QueryString 对象解析查询字符串。我的简单 QuerystringParser 类基本上是基于这里的,只做了一些微不足道的修改。

  4. JavaScript:权威指南 [David Flanagon,ISBN 0-596-00048-0,第 4 版]

    一本出色的、通用的 JavaScript 参考书。本书还包含对 W3C DOM 的有用参考。

历史

  • 05 年 2 月 - 文章发布。
© . All rights reserved.