十一、组织游戏对象

在前面的章节中,你已经看到了如何使用类来对属于同一类的变量进行分组。本章着眼于不同类型的游戏对象之间的相似性,以及如何用 JavaScript 表达这些相似性。

游戏对象之间的相似性

如果你看看画师游戏中不同的游戏对象,你会发现它们有很多共同点。例如,球、大炮和颜料罐都使用三个精灵,分别代表三种不同的颜色。此外,游戏中的大多数物体都有位置和速度。此外,所有游戏对象都需要一个方法来绘制它们,一些游戏对象有一个处理输入的方法,一些游戏对象有一个update方法、 等等。现在,这些类有相似之处并不是一个问题。浏览器或游戏玩家不会对此抱怨。但是,很遗憾,你必须一直复制代码。举个例子,BallPaintCan类都有widthheight属性:

Object.defineProperty(Ball.prototype, "width",
    {
        get: function () {
            return this.currentColor.width;
        }
    });
Object.defineProperty(Ball.prototype, "height",
    {
        get: function () {
            return this.currentColor.height;
        }
    });

代码是完全一样的,但是你必须为两个类复制它。并且每次你想添加一个不同种类的游戏对象,你可能需要再次复制这些属性。在这种情况下,幸运的是,这些属性并不复杂,但是在应用中,您还复制了许多其他内容。例如,Painter 游戏中的大多数游戏对象类都定义了以下成员变量:

this.currentColor = *some sprite*
;
this.velocity = Vector2.zero;
this.position = Vector2.zero;
this.origin = Vector2.zero;
this.rotation = 0;

各种游戏对象的draw方法看起来也很相似。例如,下面是BallPaintCan类的draw方法:

Ball.prototype.draw = function () {
    if (!this.shooting)
        return;
    Canvas2D.drawImage(this.currentColor, this.position, this.rotation, 1,
        this.origin);
};
PaintCan.prototype.draw = function () {
    Canvas2D.drawImage(this.currentColor, this.position, this.rotation, 1,
        this.origin);
};

同样,代码在不同的类中是非常相似的,你每次创建一个新的游戏对象时都要复制它。一般来说,最好避免复制大量代码。为什么会这样?因为如果在某个时候你意识到那部分代码中有错误,你必须在你复制它的地方改正它。在像 Painter 这样的小游戏中,这不是什么大问题。但是当你开发一个拥有数百个不同游戏对象类的商业游戏时,这就变成了一个严重的维护问题。此外,你并不总是知道一个小游戏会走多远。如果您不小心,您可能会复制大量代码(以及与之相关的错误)。随着游戏的成熟,留意在哪里优化代码是一个好主意,即使这意味着一些额外的工作来找到这些重复并巩固它们。对于这种特殊的情况,你需要考虑不同种类的游戏对象是如何相似的,以及你是否可以将这些相似性组合在一起,就像你在前面的章节中对成员变量进行分组一样。

从概念上讲,很容易说出球、颜料罐和大炮之间的相似之处:它们都是游戏对象。基本上都可以画在某个位置;它们都有一个速度(即使是大炮,但它的速度为零);它们都有红色、绿色或蓝色。此外,它们中的大多数处理某种类型的输入并被更新。

遗产

使用 JavaScript 中的原型,可以将这些相似之处组合在一个泛型类中,然后定义其他类,这些类是这个泛型类的特殊版本。在面向对象的行话中,这被称为继承 ,这是一个非常强大的语言特性。在 JavaScript 中,继承是通过原型机制 实现的。考虑以下示例:

function Vehicle() {
    this.numberOfWheels = 4;
    this.brand = "";
}
Vehicle.prototype.what = function() {
    return "nrOfWheels = " + this.numberOfWheels + ", brand = " + this.brand;
};

这里有一个非常简单的表示车辆的类的例子(你可以想象这对交通模拟游戏很有用)。简单来说,一辆车由多个车轮和一个品牌来定义。Vehicle类 也有一个名为what的方法,返回车辆的描述。如果您想创建一个在表格中显示车辆列表的网站,这可能会很有用。您可以按如下方式使用该类:

var v = new Vehicle();
v.brand = "volkswagen";
console.log(v.what()); // outputs "nrOfWheels = 4, brand = volkswagen"

有不同类型的交通工具,如汽车、自行车、摩托车等等。对于其中一些类型,您可能希望存储附加信息。例如,对于一辆汽车,存储它是否是敞篷车可能是有用的;对于摩托车,它有多少个气缸;等等。您可以使用 JavaScript 中基于原型的继承机制来实现这一点。下面是一个名为Car的类的例子:

function Car(brand) {
    Vehicle.call(this);
    this.brand = brand;
    this.convertible = false;
}
Car.prototype = Object.create(Vehicle.prototype);

在这个类声明中有一些新的东西。在底部,你给Carprototype对象赋值。你可以通过使用Object.create方法来做到这一点。在这种情况下,您复制了Vehicleprototype对象,并将该副本存储在Carprototype对象中。换句话说,Car现在拥有与Vehicle相同的功能,包括what方法:

var c = new Car("mercedes");
console.log(c.what()); // outputs "nrOfWheels = 4, brand = mercedes"

Car的构造函数中有下面一行:

Vehicle.call(this);

这里发生的是使用调用Car构造函数时创建的同一个对象调用Vehicle构造函数。本质上,你是在告诉解释器,你当前操作的Car对象(this)实际上也是一个 Vehicle 对象。所以你可以看到继承的两个重要方面:

  • 对象之间有关系(一个Car对象也是一个Vehicle)。
  • 从另一个类继承的类复制其功能(Car对象与Vehicle对象具有相同的成员变量、属性和方法)。

因为Car继承自Vehicle,所以你也说CarVehicle子类或者派生类,或者说VehicleCar超类,或者父类,或者基类。类之间的继承关系应用广泛;而在一个好的类设计中,可以解释为“是一种。”在这个例子中,关系很清楚:汽车是一种交通工具。反过来也不总是对的。交通工具并不总是汽车。Vehicle可能还有其他子类,例如:

function Motorbike(brand) {
    Vehicle.call(this);
    this.numberOfWheels = 2;
    this.brand = brand;
    this.cylinders = 4;
}
Motorbike.prototype = Object.create(Vehicle.prototype);

摩托车也是一种交通工具。Motorbike类从Vehicle继承而来,并添加了自己的自定义成员变量来指示气缸数。图 11-1 说明了类的层次结构。对于这个层次结构的更扩展版本,参见图 11-4

9781430265382_Fig11-01.jpg

图 11-1Vehicle及其子类的继承图

游戏对象和继承

“是一种”关系也适用于画家游戏中的游戏对象。球是一种游戏对象,颜料罐和大炮也是。你可以通过定义一个名为ThreeColorGameObject的类名,让你的游戏对象类从这个类名中继承,从而在程序中明确这种继承关系。然后你可以把所有定义三色游戏对象的东西放在那个类中,球、大炮和油漆罐将是那个类的特殊版本。

让我们更详细地看看这个ThreeColorGameObject 级。您将游戏中不同类型的游戏对象通常使用的成员变量放入这个类中。您可以如下定义该类的基本框架:

function ThreeColorGameObject() {
    this.currentColor = undefined;
    this.velocity = Vector2.zero;
    this.position = Vector2.zero;
    this.origin = Vector2.zero;
    this.rotation = 0;
    this.visible = true;
}

ThreeColorGameObject继承的每个类都有一个速度,一个位置,一个原点,一个旋转,等等。这很好,因为现在你只在一个地方定义这些成员变量,它们可以在任何继承自ThreeColorGameObject的类中使用。

这个构造函数中仍然缺少的一点是处理三种不同颜色的方法。在 Painter 的例子中,每个游戏对象类型都有三个不同的精灵,每个精灵代表一种不同的颜色。当你定义ThreeColorGameObject类时,你还不知道使用哪个精灵,因为它们将取决于游戏对象的最终类型(大炮使用精灵而不是球或油漆桶)。为了解决这个问题,让我们如下扩展构造函数:

function ThreeColorGameObject(sprColorRed, sprColorGreen, sprColorBlue) {
    this.colorRed = sprColorRed;
    this.colorGreen = sprColorGreen;
    this.colorBlue = sprColorBlue;
    this.currentColor = this.colorRed;
    this.velocity = Vector2.zero;
    this.position = Vector2.zero;
    this.origin = Vector2.zero;
    this.rotation = 0;
    this.visible = true;
}

无论何时继承这个类,都可以定义成员变量colorRedcolorGreencolorBlue的值。

现在您需要定义基本的游戏循环方法。绘制游戏对象的方法很简单。您可能已经注意到这个类中添加了一个成员变量visible。您可以使用这个成员变量来切换游戏对象的可见性。在draw方法中,只有当游戏对象应该可见时,才在屏幕上绘制精灵:

ThreeColorGameObject.prototype.draw = function () {
    if (!this.visible)
        return;
    Canvas2D.drawImage(this.currentColor, this.position, this.rotation, 1,
        this.origin);
};

该类的update方法包含一条更新游戏对象当前位置的指令:

ThreeColorGameObject.prototype.update = function (delta) {
    this.position.addTo(this.velocity.multiply(delta));
};

最后,添加一些方便的属性来获取和设置颜色,并检索对象的尺寸。例如,这是用于读取和写入对象颜色的属性:

Object.defineProperty(ThreeColorGameObject.prototype, "color",
    {
        get: function () {
            if (this.currentColor === this.colorRed)
                return Color.red;
            else if (this.currentColor === this.colorGreen)
                return Color.green;
            else
                return Color.blue;
        },
        set: function (value) {
            if (value === Color.red)
                this.currentColor = this.colorRed;
            else if (value === Color.green)
                this.currentColor = this.colorGreen;
            else if (value === Color.blue)
                this.currentColor = this.colorBlue;
        }
    });

如你所见,这里使用了彩色的 sprite 成员变量。任何从ThreeColorGameObject继承的类现在也有这个属性。这为您节省了大量的代码复制!关于完整的ThreeColorGameObject类,参见属于本章的 Painter9 示例。

Cannon 作为 ThreeColorGameObject 的子类

现在你已经为彩色游戏对象创建了一个非常基本的类,你可以通过从这个类继承来为你游戏中的实际游戏对象重用这个基本行为。我们先来看一下Cannon类。因为您已经定义了基本的ThreeColorGameObject类,所以您可以创建Cannon类作为该类的子类,如下所示:

function Cannon() {
    // to do...
}
Cannon.prototype = Object.create(ThreeColorGameObject.prototype);

通过复制ThreeColorGameObject.prototype对象来创建Cannon.prototype对象。但是,您仍然需要在构造函数方法中编写代码。

因为Cannon继承自ThreeColorGameObject,所以需要调用ThreeColorGameObject类的构造函数。此构造函数需要三个参数。因为您正在创建一个Cannon对象,所以您想要将彩色的加农炮精灵传递给该构造函数。幸运的是,你可以通过call方法传递这些精灵,如下所示:

ThreeColorGameObject.call(this, sprites.cannon_red, sprites.cannon_green,
    sprites.cannon_blue);

第二,你设置大炮的位置和原点,就像你在最初的Cannon类中所做的那样:

this.position = new Vector2(72, 405);
this.origin = new Vector2(34, 34);

剩下的工作(分配三个颜色精灵和初始化其他成员变量)已经在ThreeColorGameObject构造函数中完成了!注意,在子类中设置成员变量之前,首先调用超类的构造函数是很重要的。否则,当调用ThreeColorGameObject构造函数时,您为加农炮选择的位置和原点值将被重置为零。

现在已经定义了新版本的Cannon类,您可以开始向该类添加属性和方法,就像您之前所做的一样。例如,下面是handleInput方法:

Cannon.prototype.handleInput = function (delta) {
    if (Keyboard.down(Keys.R))
        this.currentColor = this.colorRed;
    else if (Keyboard.down(Keys.G))
        this.currentColor = this.colorGreen;
    else if (Keyboard.down(Keys.B))
        this.currentColor = this.colorBlue;
    var opposite = Mouse.position.y - this.position.y;
    var adjacent = Mouse.position.x - this.position.x;
    this.rotation = Math.atan2(opposite, adjacent);
};

如您所见,您可以毫无问题地访问成员变量,如currentColorrotation。因为Cannon继承自ThreeColorGameObject,所以它包含相同的成员变量、属性和方法。

重写超类的方法

除了添加新的方法和属性,你还可以选择用替换Cannon类中的方法。例如,ThreeColorGameObject有如下的draw方法:

ThreeColorGameObject.prototype.draw = function () {
    if (!this.visible)
        return;
    Canvas2D.drawImage(this.currentColor, this.position,
        this.rotation, 1, this.origin);
};

对于加农炮来说,这种方法并不完全如你所愿。你想画大炮的颜色,但你也想画炮管。替换一个方法非常容易。您只需将该方法重新定义为Cannon原型的一部分:

Cannon.prototype.draw = function () {
    if (!this.visible)
        return;
    var colorPosition = this.position.subtract(this.size.divideBy(2));
    Canvas2D.drawImage(sprites.cannon_barrel, this.position, this.rotation, 1,
        this.origin);
    Canvas2D.drawImage(this.currentColor, colorPosition);
};

用面向对象的行话来说,当你替换子类中从超类继承的方法时,你说你覆盖了该方法。在这种情况下,您覆盖了来自ThreeColorGameObjectdraw方法。类似地,如果您愿意,您可以覆盖一个属性,或者甚至通过让它们引用undefined来删除属性和方法。一旦创建了一个Cannon对象,您就拥有了 JavaScript 提供的修改该对象的全部灵活性。

注意即使你在这个例子中覆盖了一个方法,JavaScript 也不像 Java 或 C#等其他语言那样使用override关键字。

如果你看一看属于本章的 Painter9 示例中的Cannon.js文件,你可以看到Cannon类的定义比以前的版本小得多,也更容易阅读,因为所有通用的游戏对象成员都放在了ThreeColorGameObject类中。将代码组织在不同的类和子类中有助于减少代码复制,并使设计更加简洁。但是,有一个警告:你的类结构(哪个类从哪个类继承)必须正确。请记住,只有当类之间存在“是一种”关系时,类才应该从其他类继承。为了说明这一点,假设您想在屏幕顶部添加一个指示器,显示球当前的颜色。您可以为此创建一个类,并让它从Cannon类继承,因为它需要以类似的方式处理输入:

function ColorIndicator() {
    Cannon.call(this, ...);
    // etc.
}

然而,这是一个非常糟糕的想法。颜色指示器当然不是一种大炮,这样设计您的类会让其他开发人员非常不清楚这些类的用途。此外,颜色指示器还会旋转,这没有任何意义。类继承图应该有逻辑性并且容易理解。每当你写一个继承自另一个类的类时,问问你自己这个类是否真的是你继承的类的一种。如果不是,那么你必须重新考虑你的设计。

球课

您以与Cannon类非常相似的方式定义新的Ball类。就像在Cannon类中一样,你继承了ThreeColorGameObject类。唯一不同的是,你必须添加一个额外的成员变量来指示球当前是否正在射门:

function Ball() {
    ThreeColorGameObject.call(this, sprites.ball_red, sprites.ball_green,
        sprites.ball_blue);
    this.shooting = false;
    this.reset();
}
Ball.prototype = Object.create(ThreeColorGameObject.prototype);

当一个Ball实例被创建时,你需要调用ThreeColorGameObject构造函数,就像你对Cannon类所做的那样。在这种情况下,您将球精灵作为参数传递。另外,你需要给shooting变量一个初始值false,你通过调用reset方法来重置球。

Ball类清楚地说明了当你从另一个类继承时会发生什么。每个Ball实例由从ThreeColorGameObject继承的部分和在Ball类中定义的部分组成。图 11-2 显示了没有使用继承的Ball对象的内存的样子。图 11-3 也显示了一个Ball实例,但是使用了本章介绍的继承机制。

9781430265382_Fig11-02.jpg

图 11-2Ball类(无继承)的实例使用的内存概述

9781430265382_Fig11-03.jpg

图 11-3Ball类的一个实例(从ThreeColorGameObject继承而来)

你可能会对这两个图形和它们呈现的结构感到有点困惑。稍后,本章将更详细地讨论内存结构。现在,假设由多个成员变量组成的复杂对象(如CannonBall实例)的存储方式不同于简单的数字或布尔。这意味着什么,以及你应该如何在你的代码中正确地处理它,在这一章的结尾有所涉及。

ThreeColorGameObject类中的update方法只包含一行代码,它根据游戏对象的速度、经过的时间和当前位置来计算游戏对象的新位置:

this.position.addTo(this.velocity.multiply(delta));

球应该做得更多。球的速度应该更新,以纳入阻力和重力;球的颜色需要的话要更新;而如果球飞出了屏幕,就要复位到原来的位置。您可以简单地从先前版本的Ball类中复制update方法,这样它就可以替换ThreeColorGameObjectupdate方法。一个稍微好一点的方法是在Ball类中定义update方法,但是重用ThreeColorGameObject中最初的update方法。这可以通过使用call方法来完成,方式非常类似于您使用它来调用超类的构造函数。下面是Ball.update方法的新版本:

Ball.prototype.update = function (delta) {
    ThreeColorGameObject.prototype.update.call(this, delta);
    if (this.shooting) {
        this.velocity.x *= 0.99;
        this.velocity.y += 6;
    }
    else {
        this.color = Game.gameWorld.cannon.color;
        this.position = Game.gameWorld.cannon.ballPosition
            .subtractFrom(this.center);
    }
    if (Game.gameWorld.isOutsideWorld(this.position))
        this.reset();
};

看这个方法的第一条指令。您正在访问ThreeColorGameObjectprototype对象,它包含一个update函数。你在传递this对象的同时调用这个update函数,所以Ball对象被更新,但是根据ThreeColorGameObject中定义的update方法。最后,您将delta参数传递给该调用。好的一面是,这种方法允许您将更新过程的不同部分(在本例中)分开。任何具有位置和速度的游戏对象都需要在游戏循环的每次迭代中根据其速度更新其位置。您在ThreeColorGameObjectupdate方法中定义了这个行为,这样您就可以为从ThreeColorGameObject继承的任何类重用它!

多态性

因为有了继承机制,你不必总是知道一个变量指向什么类型的对象。考虑下面的声明和初始化:

var someKindOfGameObject = new Cannon();

在代码的其他地方,你这样做:

someKindOfGameObject.update(delta);

现在假设您更改了声明和初始化,如下所示:

var someKindOfGameObject = new Ball();

需要把调用改成update方法吗?不,你不需要,因为游戏循环方法被调用的方式是在ThreeColorGameObject类中定义的。当你在someKindOfGameObject变量上调用update方法时,它实际引用的是哪个游戏对象并不重要。唯一重要的是定义了update方法,并且它只需要一个参数:自最后一次update调用以来经过的时间。因为解释器会跟踪它是哪个对象,所以会自动调用正确版本的update方法。

这种效应被称为多态性,有时会非常方便。多态性允许您更好地分离代码。假设一家游戏公司想要发布其游戏的扩展。例如,它可能想引入一些新的敌人,或者玩家可以学习的技能。公司可以将这些扩展作为泛型EnemySkill类的子类来提供。实际的游戏代码将会使用这些对象,而不需要知道它在处理哪种特殊技能或敌人。它只是调用泛型类中定义的方法。

类的层次结构

在这一章中,你已经看到了几个从基本游戏对象类继承的类的例子。只有当这两个类之间的关系可以描述为“是一种”时,一个类才应该从另一个类继承比如:a BallThreeColorGameObject的一种。事实上,等级制度并没有到此为止。你可以写另一个继承自Ball类的类,比如BouncingBall,它可以是一个标准球的特殊版本,可以从油漆罐上反弹,而不仅仅是与它们碰撞。你还可以创建另一个继承自BouncingBall的类BouncingElasticBall,它是一个球,当它在油漆桶上反弹时会根据它的弹性变形。每次从一个类继承时,都可以免费从基类中获得数据(编码在成员变量中)和行为(编码在方法和属性中)。

商业游戏有一个不同游戏对象的等级体系,有许多不同的级别。回到本章开始的交通模拟例子,你可以想象一个非常复杂的各种不同车辆的层次结构。图 11-4 显示了这样一个层次结构的例子。该图使用箭头来指示类之间的继承关系。

9781430265382_Fig11-04.jpg

图 11-4 。交通模拟游戏中复杂的游戏对象层次

在继承树的最底层是一个GameObject类。这个类只包含非常基本的信息,比如游戏对象的位置或速度。对于每个子类,可以添加与特定类及其子类相关的新成员(变量、方法或属性)。例如,变量numberOfWheels通常属于Vehicle类,而不属于MovingGameObject(因为船没有轮子)。变量flightAltitude属于Airplane类,变量bellIsWorking属于Bicycle类。

当你决定你的类的结构时,你必须做出许多决定。没有单一的最佳等级;而且,根据应用的不同,一种层次结构可能比另一种更有用。例如,这个例子首先根据物体用来移动自身的媒介来划分MovingGameObject类:土地、空气或水。之后,这些类又分为不同的子类:机动化或非机动化。你可以反过来做这件事。对于某些类,它们在层次结构中的位置并不完全清楚:你说摩托车是一种特殊类型的自行车(有马达的那种)吗?还是一种特殊的机动车辆(只有两个轮子的那种)?

重要的是,类本身之间的关系是清晰的。帆船是船,但船并不总是帆船。自行车是一种交通工具,但不是每一种交通工具都是自行车。

值与参考值

在你读完这一章之前,让我们看看对象和变量是如何在内存中被处理的。当处理基本类型如数字或布尔时,变量与内存中的位置直接相关。比如看下面的声明和初始化:

var i = 12;

该指令执行后,存储器看起来如图图 11-5 所示。

9781430265382_Fig11-05.jpg

图 11-5 。数字变量的内存使用

现在您可以创建一个新变量j并将变量i的值存储在该变量中:

var j = i;

图 11-6 显示了执行该指令后内存的样子。

9781430265382_Fig11-06.jpg

图 11-6 。声明和初始化两个数字变量后的内存使用情况

如果你给j变量赋另一个值,例如通过执行指令j = 24,产生的内存使用如图图 11-7 所示。

9781430265382_Fig11-07.jpg

图 11-7 。更改j变量值后的内存使用

现在让我们看看当您使用更复杂类型的变量时会发生什么,比如Cannon类。考虑以下代码:

var cannon1 = new Cannon();
var cannon2 = cannon1;

看一下前面使用数字类型的例子,您会期望现在内存中有两个Cannon对象:一个存储在变量cannon1中,另一个存储在cannon2中。然而,事实并非如此!其实cannon1cannon2T7 都是指同一个物体。第一条指令后(创建Cannon对象),内存如图图 11-8 所示。

9781430265382_Fig11-08.jpg

图 11-8 。内存中的一个Cannon对象

在这里,您可以看到基本类型(如数字和布尔值)与更复杂的类型(如Cannon类)在内存中的表示方式有很大的不同。在 JavaScript 中,所有非原始类型的对象,比如数字、布尔值和字符,都存储为引用而不是值。这意味着像cannon1这样的变量并不直接包含Cannon对象,但是它包含了对它的引用图 11-8 通过将cannon1表示为一个包含指向一个对象的箭头的块来表示它是一个引用。如果你现在声明了cannon2变量并将cannon1的值赋给它,你可以在图 11-9 中看到新的情况。

9781430265382_Fig11-09.jpg

图 11-9 。指向同一个对象的两个变量

结果是,如果你改变加农炮的颜色如下

cannon2.color = Color.red;

那么表达式cannon1.color将是Color.red,因为cannon1cannon2指的是同一个对象!这对对象在方法中的传递方式也有影响。例如,ThreeColorGameObject的构造函数方法期望三个精灵作为参数。因为精灵不是 JavaScript 中的基本类型,所以您实际上是在传递对这些精灵的引用。理论上,这意味着您可以在ThreeColorGameObject构造函数中修改精灵。将基本类型(比如数字)作为参数传递给方法是通过值发生的,所以改变方法中的值没有影响。考虑下面的函数

function square(f) {
    f = f * f;
}

现在是以下指令:

var someNumber = 10;
square(someNumber);

执行完这些指令后,someNumber的值仍然是 10(而不是 100)。这是为什么?因为当调用square函数时,number 参数通过值传递给。变量f是方法中的一个局部变量,最初包含变量someNumber的值。在该方法中,局部变量f被更改为包含f * f,但这不会更改someNumber变量,因为它是内存中的另一个位置。因为非原始对象是通过引用传递的,所以下面的示例将导致对象的更改值作为参数传递:

function square(obj) {
    obj.f = obj.f * obj.f;
}

var myObject = { f : 10 };
square(myObject);
// myObject.f now contains the value 100.

每当 JavaScript 脚本运行时,内存中都有大量的引用和值。例如,如果您查看图 11-211-3 ,您会看到Ball对象既包含值,也包含对其他对象的引用(例如Vector2对象或Image对象)。

空的和未定义的

每当您在 JavaScript 中声明一个变量时,最初它的值被设置为undefined :

var someVariable;
console.log(someVariable); // will print 'undefined'.

在 JavaScript 中,你也可以指出一个变量被定义了,但是当前没有引用任何对象。这是通过使用null关键字完成的:

var anotherCannon = null;

因为你还没有创建一个对象(使用new关键字),内存看起来像图 11-10 中描述的那样。

9781430265382_Fig11-10.jpg

图 11-10 。一个指向null的变量

因此,指示一个变量还没有指向任何东西是通过给它赋值null来完成的。甚至可以在 JavaScript 程序中检查变量是否指向一个对象,就像这样:

if (anotherCannon === null)
    anotherCannon = new Cannon();

在这个例子中,你检查变量是否等于null(没有指向一个对象)。如果是这样,你使用new关键字创建一个Cannon实例,之后内存中的情况再次改变(见图 11-11 )。

9781430265382_Fig11-11.jpg

图 11-11 。记忆中的最后情境

由您决定何时使用nullundefined。不是所有的程序员都用同样的方式做这件事。我们建议你用undefined来表示一个变量不存在,用null来表示这个变量存在但还没有引用任何对象。

你学到了什么

在本章中,您学习了:

  • 如何使用继承来构建层次结构中的相关类
  • 如何重写子类中的方法来为该类提供特定的行为
  • 如何从超类中调用方法,比如构造函数方法
  • nullundefined的含义