45. JavaScript 中的对象

  • 打印

引言

前面的函数一文引入了函数的概念,向我们讲授了如何将单个的程序任务划分成逻辑块,这样就可以在任何地方调用这些逻辑块,从而更好地组织和重复使用我们的代码。既然我们已经熟悉了这些 JavaScript 程序设计的基础知识,下面我们就可以更进一步,本文中我们会引入对象的概念。通过对象,你可以将自己定义为函数的相关功能集聚拢在一起,并将它们打成一个包,你可以把这个包像一个整体一样传送和引用。这种能力对于你所编写的代码来说有着非常实际的意义,尽管现在听起来有点抽象。

你可能还没有注意到,但在本系列的整个教程中你已经不知不觉地接触过对象了;在这里,我会更明白地告诉你在 JavaScript 中对象是如何工作的,还会教你如何利用对象来提高你的代码的可读性和可复用性。

 

本文结构如下:

注:你可以在此处下载一个示例,也可以实际运行它,该示例包括了用于计算一个三角形的面积的代码,既有使用了对象的版本,也有不使用对象的版本。在下面的文章中我们会逐步创建该程序。点击此处运行该三角形对象示例

为什么要用对象?

关注对象的一个最重要的原因就是,它们能够改善你的代码对你所要实现的数据和处理过程的表达。作为一个简单的例子,我们来设想一下你该怎样编写代码以对一个三角形进行一些处理。我们知道三角形一般有三条边,因此为了对某个特定的三角形进行处理,显然需要创建三个变量:

// 这是一个三角形。
var sideA = 3;
var sideB = 4;
var sideC = 5;

现在,我们就有了一个三角形了!但是这还不够,对不对?我们其实只是创建了三个需要分别记录的变量,以及一个用来提醒自己此处是什么的注释而已。这样的三角形定义还不够清楚,或者说还不太好用。但是没关系,让我们继续,看看我们该怎样围绕该“三角形”来创建一些运算。为了得出该三角形的面积,你可能会写出如下的函数:

function getArea( a, b, c ) {
  // 通过海伦公式来计算一个三角形的面积,并返回面积的值
        
  var semiperimeter   =   (a + b + c) / 2;
  var calculation     =   semiperimeter * (semiperimeter - a) * (semiperimeter - b) * (semiperimeter - c);
  return Math.sqrt( calculation );
}

alert( getArea( sideA, sideB, sideC ) );

你会发现自己需要将所有关于该三角形的信息传递给函数,从而让函数来进行计算。与三角形相关的操作与该三角形的数据被完全分离了,即使在分离状态下这些操作并没有什么实际意义。

此外,我用了一个很通用的名字来作为函数名,也给每个变量定义了一个通用的变量名:getAreasideA,等等。如果下一周我发现自己需要将这个程序扩展为可以将矩形也包含在内,又会发生什么呢?我想用 sideA sideB 来代表该矩形的数据,但这两个变量名已经被占用了。我可以用 side1 side2 ,但我敢肯定,你也能看出这么做会导致混淆和彻底失败。可能我最终会用 rectangleSideA rectangleSideB ,而且为了保持一致性,我也只能退回去修改所有已经写好的三角形的代码,改成用 triangleSideA 等等,这会导致某些潜在的出错风险。对于函数名来说也一样:我想用 getArea 来作这两种图形的函数名,因为从概念上说它进行的是同样的计算,但是我无法做到。肯定有一种更好的方式来表达我的数据的!

我们曾经很明智地通过创建函数,将一系列指令打包成一个独立的、命名严谨的操作,同样地,此处可以通过创建对象来将所有的“东西”集合成一个独立的单元。通过对象,我们就可以建立属于自己的包含任意数量、任意类型变量的容器,而不用受限于 JavaScript 自带的原始数据类型(字符串型,数字型,布尔型,等等)。这种自由机动性使我们可以创建一些结构,这些结构可以直接映射到编写程序时我们所要关注的“东西”上,在我们的代码中可以直接应用这些结构,就像使用自带的原始数据类型一样。在这里,我要创建的是三角形对象和矩形对象,每个对象分别包含了对该形状进行智能化处理所需的一切数据,以及可能要对该图形执行的所有操作。为了实现这个目标,我们来学习一些语法。

似曾相识之处

如果你回顾一下前一篇文章中的最后一个函数示例,你就会发现像这样的代码片段:

var obj = document.getElementById( elementID );

还有:

obj.style.background = 'rgb('+red+','+green+','+blue')';

意外吧!你已经用到过对象了,而你甚至都不知道自己用的是对象!在全面讲述 JavaScript 的对象语法之前,我们先来详细研究一下这两段代码。

这句代码 var obj = document.getElementById( elementID )在某种程度上应该是似曾相识的。我们知道一句指令末尾的圆括号意味着正在执行某种类型的函数,而且可以看到这个函数的调用结果被存储在名叫 obj 的变量中。此处唯一的一点东西就是中间那个点号。在 JavaScript 中,点号就是访问一个对象的内部数据的方式。点号(.)其实就是位于运算对象之间的一个操作符,就跟+号和-号一样。

根据惯例,可以通过点号操作符访问的存储于某个对象之中的变量一般叫做属性。如果属性同时又刚好是函数的话,就叫做方法。这两个词语都没什么奇特的;方法只不过就是函数,而属性只不过是变量而已。

点号操作符左侧必须是一个对象,右侧则是一个属性名;上面的代码片段的含义就是访问内置的 document 对象的 getElementById 方法(关于这种用法,将在遍历 DOM 一文中详细介绍)。

第二段代码要更有意思一点:它有两个点号。 JavaScript 的对象支持真正令人激动的特性之一就是可将点号连在一起使用,以访问复杂的结构。简而言之,你可以同样地将对象连在一起,就像你执行 var x = 2 + 3 + 4 + 5;,会得到值为14的结果一样。对象的连续引用会直接对自身进行解析,方向为从左到右(如果你跟你的朋友说,这一点使得 JavaScript 的点号操作符成为“左关联中缀操作符”的话,一定会让他们印象深刻)。在本例中,我们对 obj.style 进行了求值,并将其解析成一个对象,然后访问该对象的 background 属性。如果你愿意的话,你可以在自己的代码中添加圆括号来显式地表示这一点:(obj.style).background

创建对象

我将用下面的语法来显式地创建我自己的三角形对象:

var triangle = new Object();

triangle 现在是一个空白的地基,等着我们去修建一个有高高耸立的三条边的宏伟建筑。通过使用点号操作符来为自己的对象添加属性,你就可以实现这个目标:

triangle.sideA  =   3;
triangle.sideB  =   4;
triangle.sideC  =   5;

要向一个对象添加新的属性,你实际上并不需要做什么特殊的工作。JavaScrip 在解析点号操作符时是很宽容的。如果你打算设置一个不存在的属性的话, JavaScript 会替你创建它。如果你试图读取不存在的某个属性的话, JavaScript 会返回 “undefined” 。这样的特性的确很方便,但如果你不仔细的话也会犯错误,因此在打字的时候要小心!

向对象添加方法也是一样的——下面是一个例子:

triangle.getArea    =   function ( a, b, c ) {
  // 通过海伦公式来计算一个三角形的面积,并返回面积的值
        
  var semiperimeter   =   (a + b + c) / 2;
  var calculation     =   semiperimeter * (semiperimeter - a) *
                                (semiperimeter - b) * (semiperimeter - c);
  return Math.sqrt( calculation );
        
};      // 注意此处的分号;这是必需的。

如果你觉得这些代码看起来跟函数定义非常相似,你算是说对了:我直接将函数名完全沿用了下来。 JavaScript 有一个匿名函数的概念,这种函数自己并没有名字,但是它跟其它的值一样可以被存储在某个变量中。在这段代码中,我创建了一个匿名函数,并将其存储在 triangle 对象的 getArea 属性中。这样,这个对象就可以随身携带该函数了,就像携带其它所有属性一样。

自我引用

创建 triangle 对象的目的之一,就是在三角形的数据和在这些数据之上所能执行的操作之间创建一种联系。然而,我还没有完成这一点。你马上就会注意到 triangle.getArea 方法在执行时还需要传入边长数据,这样需要如下所示的代码:

triangle.getArea( triangle.sideA, triangle.sideB, triangle.sideC );

我觉得这样做比本文开头的那段代码要更好,因为它明确地表达了数据和操作之间的关系。不过,三角形面积和边长的关系意味着我们不必告诉此方法所要处理的值,它应该能够搜集自己所需要的关于该对象的数据并使用该数据,而不需要你手动输入并传给它。

解决此问题的关键就在于 this 关键字,你可以在对象方法的定义中使用此关键字,来引用同一对象中的其它属性或方法。通过用 this 来重写 getArea 方法,我们最终会得到如下的代码:

triangle.getArea    =   function () {
  // 通过海伦公式来计算一个三角形的面积,并返回面积的值
        
  var semiperimeter   =   (this.sideA + this.sideB + this.sideC) / 2;
  var calculation     =   semiperimeter * (semiperimeter - this.sideA) * (semiperimeter - this.sideB) * (semiperimeter - this.sideC);
                                
  return Math.sqrt( calculation );
        
};      // 注意此处的分号;这是必需的。

如你所见,this 的作用在某种意义上就像一面镜子一样。当 getArea 方法获得执行的时候,它会读取自己的 sideAsideBsideC 属性。这样它就能够将这些值运用在对象方法中,而不用依赖于从外部获取的输入信息了。

注:这述说明有点过于简化了。this 所引用的并非总是定义此方法的对象,而会根据特定环境而改变。对于此处的含糊我只能说声抱歉,但这有点超出本文所关注的范围了。在此例中你大可放心,此处的 this 一直是指向 triangle 对象的。

作为关联数组的对象

点号操作符并不是访问一个对象的属性和方法的唯一办法;使用下标也可以非常有效地访问对象的属性和方法,在前面的关于数组的讨论中你可能已经熟悉了下标。简而言之,你可以将一个对象作为一个关联数组来对待,该关联数组将一个字符串映射到一个值,就像一般的数组将一个数字映射到一个值一样。通过利用这个标号,你就可以用另一种方式重写 triangle

var triangle = new Object();
triangle['sideA']   =   3;
triangle['sideB']   =   4;
triangle['sideC']   =   5;
triangle['getArea'] =   function ( a, b, c ) {
  // 通过海伦公式来计算一个三角形的面积,并返回面积的值
        
  var semiperimeter   =   (a + b + c) / 2;
  var calculation     =   semiperimeter * (semiperimeter - a) * (semiperimeter - b) * (semiperimeter - c);
  return Math.sqrt( calculation );
        
};      // 注意此处的分号;这是必需的。

乍一看,这样做似乎有点多余。为什么不干脆就用点号呢?这种新语法的好处在于属性名不是硬编码到你的程序中的。你可以用变量来指定属性名,这就意味着你可以创建更灵活的指令,这样的指令可以基于上下文来完成不同的任务。比如说,你可以创建一个函数来比较两个对象,看看它们有没有某个共同的属性:

function isPropertyShared( objectA, objectB, propertyName ) {
  if (
     typeof objectA[ propertyName ] !== undefined
     &&
     typeof objectB[ propertyName ] !== undefined
     ) {
         alert("Both objects have a property named " + propertyName + "!");
       }
}

这个函数要是通过一般的使用点号的方法的话,是没办法写成的,因为这样我们就得在程序代码中显式地写出需要进行测试的属性的名称。你以后会经常用到这种语法的。

注:关联数组在在Perl中叫做“hash”,在C#中叫“hashtable”,在C++中叫做“map”,Java中叫“hashmap”,Python又叫做“dictionary”,等等。关联数组在程序设计中是一种非常强大而且基础性的概念,你可能早已经知道它了,只不过名称不同而已。

对象常量

下面我们要仔细研究一些看起来可能非常的眼熟的代码:

alert("Hello world");

你可以立刻辨认出 alert 是一个函数,该函数唯一的参数:字符串 “Hello world”。此处应当注意到的是,你不必这样写:

var temporaryString = "Hello world";
alert(temporaryString);

JavaScript 知道所有包含在一对双引号(" ")之中的内容都应当作为字符串来处理,无论你在何处写下该字符串, JavaScript 都会进行必要的处理从而使该字符串生效。该字符串的创建与传入该函数是同时进行的。在形式上,"Hello world"是作为一个字符串常量来引用的;为了创建字符串常量,你需要将字符串常量的内容逐字打出来。

JavaScript 也有一个类似的“对象常量”语法,该语法使你能创建自己的对象,而不需要任何额外的语法。我们来重写一下前面那个按照第三种方式创建的对象,这次通过对象常量来创建它:

var triangle = {
  sideA:      3,
  sideB:      4,
  sideC:      5,
  getArea:    function ( a, b, c ) {
    // 通过海伦公式来计算一个三角形的面积,并返回面积的值
        
    var semiperimeter   =   (a + b + c) / 2;
    var calculation     =   semiperimeter * (semiperimeter - a) * (semiperimeter - b) * (semiperimeter - c);
    return Math.sqrt( calculation );
        
  }
};

此处的语法很清楚:该对象常量用大括号来表明自己的开头和结尾,括号中包含任意数量的 propertyName: propertyValue 对,彼此之间用逗号隔开。这样就能很方便地在你程序中建立对象,而不需要在每一行定义时重复对象名了。

然而,还有一件事情应该注意:我们最常犯的错误就是,在对象常量的属性列表中的最后一项后面多加上一个逗号(在本例中,就是在getArea定义后面)。逗号只能放在属性之间——如果在末尾有个多余的逗号的话,就会导致程序出错。尤其是在程序写好后再回去代码中插入或删除的时候,你得多加小心了,保证每个逗号位置正确。

总结——还要很多东西要学

本文仅仅只触及了 JavaScript 对象的表面。读完本文后,你应该能够得心应手地创建自己的对象,添加属性和方法,并以自我引用的方式来使用它们了。本文尚未涉及到的东西还有很多,但未涉及的内容不影响你开始 JavaScript 对象之旅。本文旨在引导你上路,并为你提供所需的工具,以便你可以在今后深入钻研这一方面的课题时读懂所遇到的代码。

延伸阅读

练习题

  • 什么时候你应该用下标代替点号来引用某个对象的属性?
  • 对象如何引用自身?为什么这一点非常有用?
  • 什么是对象常量?在创建对象常量的时候,逗号应该放在哪儿?
  • 我创建了一个对象来代表一个三角形,并对其面积进行了计算。我希望你也能照样创建一个矩形对象并计算其面积。在该矩形的getArea方法中用this来避免不必要的数据传递。