首页 理论教育 JavaScript面向对象编程实践

JavaScript面向对象编程实践

时间:2023-10-18 理论教育 版权反馈
【摘要】:本附录部分将对JavaScript到底是如何支持面向对象编程的以及如何高效利用这些特性进行面向对象的JavaScript开发进行初步介绍。这些例子也表明,同java或者C#对象相比,JavaScript对象更加具有可塑性。如果记住了JavaScript对象就是字典的话,就不会对此感到奇怪了。JavaScript中的函数实际上就是对象,只不过这个对象具有同其相关联的一段可执行代码。例附录2.4:这是JavaScript中定义函数最常用的方式了。注意,JavaScript中的函数是对象。但是JavaScript不一样,它并没有类的概念。

JavaScript面向对象编程实践

面向对象的编程(OOP)方法广泛用于多种JavaScript库,采用这种方法可使代码库更加易于管理和维护。JavaScript支持OOP,但它的支持方式同流行的C++,C#,java等语言完全不同,所以,大量使用这些语言的开发者起初可能会发现,JavaScript中的OOP比较怪异,同直觉不符。本附录部分将对JavaScript到底是如何支持面向对象编程的以及如何高效利用这些特性进行面向对象的JavaScript开发进行初步介绍。

1.JavaScript对象是字典

在java或C#中,当谈及对象时,我们指的是类或者结构的实例。对象根据实例化出它的模板(也即,类)的不同而具有不同的属性和方法。JavaScript对象不是这样的。在JavaScript中,对象仅仅是name/value对的集合,我们可以把JavaScript对象看作字典,字典中的键为字符串。我们可以用我们熟悉的"."(点)操作符或者一般用于字典的"[]"操作符,来获取或者设置对象的属性。

请看示例。

例附录2.1:

上面的示例与下面的示例是等价的。

例附录2.2:

我们还可以直接在userObject的定义中定义lastLoginTime属性。

例附录2.3:

请注意这与C#和java中为字典集合添加键值对非常相似。区别在于,JavaScript中的对象/字典只接受字符串作为键,而C#和java中字典则无此限制。

这些例子也表明,同java或者C#对象相比,JavaScript对象更加具有可塑性。属性lastLoginTime不必事先声明,如果在使用这个属性的时候userObject还不具有以此为名的属性,就会在userObject中把这个属性添加进来。如果记住了JavaScript对象就是字典的话,就不会对此感到奇怪了。我们随时都可以把新键(及其对应的值)添加到字典中去,本来就如此。

JavaScript对象的属性就是这个样子的。那么,JavaScript对象的方法呢?和属性一样,JavaScript仍然和Java或C#不同。为了理解对象的方法,就需要首先仔细看看JavaScript函数。

2.JavaScript中的函数具有首要地位

在许多编程语言中,函数和对象一般都认为是两种不同的东西,如在C#中,方法作为一种函数,是属于对象的一个成员。可在JavaScript中,它们之间的区别就没有那么明显了。JavaScript中的函数实际上就是对象,只不过这个对象具有同其相关联的一段可执行代码。

请看示例。

例附录2.4:

这是JavaScript中定义函数最常用的方式了。除此之外,还可以先创建一个匿名函数对象再将该对象赋值变量func,定义出完全相同的函数。

请看示例。

例附录2.5:

甚至通过使用Function构造器,可以以字符串表示参数和函数体。

请看示例。

例附录2.6:

这表明,函数实际上就是一个支持函数调用操作的对象,这一点与C#的委托对象类似。这种使用Function构造器来定义函数的方式并不常用,但却为我们带来很多很有趣的可能,即你可以在JavaScript运行的时候构造出任意的函数。其原因是:在这种函数定义的方式中,函数体只是Function构造器的一个字符串类型的参数。

要进一步证明函数是对象,你可以就像为任何其他JavaScript对象一样,为函数设置或添加属性。

请看示例。

例附录2.7:

作为对象,函数还可以赋值给变量、作为参数传递给其他函数、作为其他函数的返回值、保存为对象的属性或数组中的一员等。

请看示例。

例附录2.8:

所以,为对象添加方法就很简单了:只要选择一个函数名并把一个函数赋值给这个函数名即可。

下面的示例通过将3个匿名函数分别赋值给各自相应的方法名,为一个对象定义了3个方法。

例附录2.9:

函数displayFullName中“this”关键字的用法对java和C#开发者来说并不陌生,该方法是通过哪个对象调用的,它指的就是哪个对象。因此在上面的例子中,displayFullName中“this”的值指的就是myDog对象。但是,“this”的值不是静态的。如果通过别的对象对函数进行调用,“this”的值也会随之指向这个别的对象。

例附录2.10:

最后一行的代码是将函数作为一个对象的方法进行调用的另外一种方式。注意,JavaScript中的函数是对象。每个函数对象都有一个call()方法,这个方法会将函数作为该方法的第一个参数的方法进行调用。也就是说,无论将哪个对象作为第一个参数传递给call()方法,它都会成为此次函数调用中“this”的值。

要特别注意的是,永远不要调用不属于任意对象却包含有“this”的函数。这是因为在这种调用中,“this”将指向Global对象,这样会搅乱全局命名空间。

例如,下面的脚本将会改变JavaScript的全局函数isNaN的行为。应该反对这么做。

例附录2.11:

到此我们已经看过了创建对象并为其添加属性和方法的几种方式。但是,如果你仔细观察以上所举的示例就会发现,所有的属性和方法都是在对象的定义之中通过硬性编码定义的。如果你需要对对象的创建进行更加严格的控制,那该怎么办?例如,你可能会需要根据某些参数对对象属性中的值进行计算,或者你可能需要将对象的属性初始化为只有到代码运行时才会得到的值,你还有可能需要创建一个对象的多个实例,这些要求也是非常常见的。

在C#或java中,我们使用类实例化得到对象实例。但是JavaScript不一样,它并没有类的概念。但是,你可以将函数同“new”操作符一起使用就可以把函数当作构造器来用。

3.有构造函数但没有类

JavaScript中的OOP最奇怪的事,如前所述,就是JavaScript没有C#和java中所具有的类。在C#中,通过如下代码。

能够得到一个对象,这个对象就是Dog类的一个实例。但在JavaScript中根本就没有类。要想得到同类最近似的效果,可以像下面这样定义一个构造器函数。

例附录2.12:

先仔细看看这行代码:

“new”操作符所做的事情很简单。首先,它会创建出一个新的空对象。然后,紧跟其后的函数调用就会得到执行,并且会将那个新建的空对象设置为该函数中“this”的值。换句话说,这行带有“new”操作符的代码可以等价于下面的代码。

在DogConstructor的函数体中可以看出,调用该函数就会对调用中关键字“this”所指的对象进行初始化。采用这种方式,你就可以为对象创建模板了!无论何时,当你需要创建类似的对象时,你就可以用“new”来调用该构造器函数,然后你就能够得到一个完全初始化好的对象。这和类看上去非常相似。实际上,JavaScript中构造器函数的名字往往就是你想模拟的类的名字,所以上面例子中的构造函数就可以直接命名为Dog。

例附录2.13:(www.xing528.com)

上面在Dog的定义中,先定义了一个叫作name的实例变量。将Dog作为构造器函数使用而创建的每个对象都有自己的一份叫作name的实例变量(如前所述,name就是该对象的字典入口)。这符合我们的期望:毕竟每个对象都需属于自己的一份实例变量,只有这样才能保存它自己的状态。但是如果你再看接下来的那行代码,就会发现Dog的每个实例都有自己的一份respondTo()方法,这是个浪费;respondTo的实例你只需要一个,只有将这一个实例在所有的Dog实例间共享即可!你可以把respondTo的定义从Dog中拿出来,这样就可以克服此问题了,如下所示。

例附录2.14:

这样一来,Dog的所有实例(也即,用构造器函数Dog创建的所有实例)都可以共享respondTo()方法的同一个实例了。但是,随着方法数量的增加,这种方式维护起来会越来越困难。最后你的代码库中会堆积大量的全局函数,而且,随着“类”的数量不断增加,特别是这些类的方法具有类似的方法名时,情况会变得更加糟糕。这里还有一个更好的办法,就是使用原型对象,可以解决这一问题。

4.原型(Prototype)

原型对象是JavaScript面向对象编程中的一个核心概念。原型这个名称来自于这样一个概念:在JavaScript中,所有对象都是通过对已有的样本(也即,原型)对象进行复制而创建的。该原型对象的所有属性和方法都会成为通过使用该原型的构造函数生成的对象的属性和方法。你可以认为,当你像这样来创建一个新的Dog对象时,这些对象从它们的原型中继承了相应的属性和方法。

buddy所引用的对象将从它的原型中继承到相应的属性和方法,虽然仅从上面这一行代码可能会很难看出来其原型来自哪里。buddy对象的原型来自构造器函数(在此例中指的就是函数Dog)的一个属性。

在JavaScript中,每个函数都有一个叫作“prototype”的属性,该属性指向一个原型对象。反过来,该原型对象据有一个叫作“constructor”的属性,该属性又指回了这个函数本身。这是一种循环引用;如图附录2.1所示更好地揭示出了这种环形关系。

图附录2.1 环形关系

当一个函数(比如上例中的Dog)和“new”操作符一起使用,创建出一个对象时,该对象将从Dog.prototype中继承所有的属性。在图附录2.1中,你可以看出,Dog.prototype对象具有一个指向Dog函数的construtor属性,每个Dog对象(它们继承自Dog.prototype)将同样也具有一个指向Dog函数的constructor属性。

下面的示例说明了它们之间的关系。

例附录2.15:

构造器函数、原型对象以及用它们创建出来的对象这三者之间的关系如图附录2.2所示。

图附录2.2 关系

你可能已经注意到了上图中对hasOwnProperty()方法和isPrototypeOf()方法的调用。这些方法又来自哪里呢?它们并不是来自Dog.prototype。实际上,JavaScript中还有其他一些类似于toString()、toLocaleString()和valueOf()等我们可以直接对Dog.prototype以及Dog的实例进行调用的方法,但它们统统都不是来自于Dog.prototype的。其实就像.NET框架具有System.Object一样,JavaScript中也有Object.prototype,它是所有类的顶级的基类。(Object.prototype的原型为null。)

在这个例子中,请记住Dog.prototype也是一个对象。它也是通过对Object的构造函数进行调用后生成的,虽然这一点在代码中并不直接出现。

所以,就如同Dog的实例继承自Dog.prototype一样,Dog.prototype继承自Object.prototype。这就使得Dog的所有实例也都会继承Object.prototype的方法和实例。

每个JavaScript对象都会继承一个原型链,该链的最末端都是Object.prototype。请注意,到此为止你在这里所见到的继承都是活生生的对象间的继承。这同你通常所认识的类在定义时形成的继承的概念不同。因此,JavaScript中的继承要来得更加的动态化。继承的算法非常简单,可以这样理解:当你要访问一个对象的属性/方法时,JavaScript会首先判断该属性/方法是否定义于该对象之中。如果不是,接下来就要对该对象的原型进行检查。如果还没有发现相应的定义,然后就会对该对象的原型的原型进行检查,并以此类推,直到碰到Object.prototype。

如图附录2.3所示即为这个解析过程。

图附录2.3 解析过程

JavaScript这种动态解析属性访问和方法调用的方式将对JavaScript带来一些影响。对原型对象的修改会马上在继承它的对象中得以体现,即使这种修改是在对象创建后才进行的也无关紧要。如果你在对象中定义了一个叫作X的属性/方法,那么该对象原型中同名的属性/方法就会无法访问到。例如,你可以通过在Dog.prototype中定义一个toString()方法来对Object.prototype中的toString()方法进行重载。所有修改只在一个方向上产生作用,即从原型到继承它的对象这个方向,相反则不然。

下面的示例演示了这种影响。

例附录2.16:

上面的示例还演示了如何解决前文碰到的避免不必要的方法实例问题。不用让每个对象都具有一个单独的方法对象的实例,你可以通过将方法放到其原型之中来让所有对象共享同一个方法。此例中,getBreed()方法由rover和spot共享,至少直到在spot中重载了getBreed()方法之前。spot在重载之后就具有自己版本的getBreed()方法,但是rover对象以及随后使用new和GreatDane创建的对象仍将继承的是定义于GreatDane.prototype对象的getBreed()方法。

5.静态属性和方法

有些时候你会需要与类而不是实例捆绑到一起的属性或方法——静态属性和静态方法。在JavaScript中这很容易就能做到,因为函数就是对象,所以可以随心所欲为其设置属性和方法。既然构造器函数在JavaScript代表了类这个概念,所以你可以通过在构造器函数中设置属性和方法来为一个类添加静态方法和属性,就像下面的示例一样。

例附录2.17:

在JavaScript调用静态方法的语法实际上和C#完全相同。既然构造器函数就是类的名字,所以这也不应该有什么奇怪的。

到目前为止,你已经有了类、公共的实例属性/方法以及静态属性/方法。你还需要什么呢?当然,还需要私有成员。但是,JavaScript并不直接支持私有成员(这方面它也不支持protected成员)。对象的所有属性和方法所有人都可以访问得到。这里有一种在类中定义出私有成员的方法,但要完成这个任务就需要首先对闭包有所了解。

6.闭包

JavaScript中更加高级的一个特性便是它对闭包的支持,在C#2.0中是通过匿名方法对闭包提供支持的。闭包是一种运行时的现象,它产生于内部函数(在C#中成为内部匿名方法)绑定到了其外部函数的局部变量之上的时候。显然,除非内部函数可以通过某种方式在外部函数之外也可以让其可以访问得到,否则这也没有多大意义。举个例子就可以把这个现象说得更清楚了。

假如你需要基于一个简单评判标准对一个数字序列进行过滤,该标准就是大于100的数字可以留下,但要把其他的所有数字都过滤掉。你可以编写一个如下所示的函数。

例附录2.18:

但是现在你想新建一个不同的过滤标准,比方说,这次只有大于300的数字才能留下。你可以这么做:

可能还需要留下大于50、25、10、600等的数字。然而,我们很快就会发现它们使用的都是“大于”这同一个谓词,所不同的只是其中的数字。所以,你可以把具体的数字拿掉,编写出这么一个函数:

有了这个函数你就可以像下面这样做了:

请注意makeGreaterThanPredicate函数所返回的内部匿名函数。该匿名内部函数使用了lowerBound,它是传递给makeGreaterThanPredicate的一个参数。根据通常的变量范围规则,当makeGreaterThanPredicate函数退出后,lowerBound就离开了它的作用范围!但是在此种情况下,内部匿名函数仍然还携带着它,即使makeGreaterThanPredicate早就退出了也还是这样。这就是我们称之为闭包的东西,因为内部函数关闭着它的定义所在的环境(也即,外部函数的参数和局部变量)。

有人认为,闭包也许没什么大不了的。但是如果使用得当,使用它可以在将你的想法转变为代码时,为你打开很多非常有意思的新思路。在JavaScript中闭包最值得关注的用途之一就是用它来模拟出类的私有变量。

7.模拟私有属性

现在让我们来看看在闭包的帮助下怎样才能模拟出私有成员。函数中的私有变量通常在函数之外是访问不到的。在函数执行结束后,实际上局部变量就会永远消失。然而,如果内部函数捕获了局部变量的话,这样的局部变量就会继续存活下去。这个就是在JavaScript中模拟出私有属性的关键所在。请看下面的Person类。

例附录2.19:

参数name和age对构造器函数Person来说就是局部变量。一旦Person函数返回之后,name和age就应该被认为永远消失了。然而,这两个参数被4个内部函数捕获,这些内部函数被赋值为Person实例的方法,因此这样一来就使得name和age能够继续存活下去,但却被很严格地限制为只有通过这4个方法才能访问到它们。所以,你可以这样做:

例附录2.20:

对于不必在构造器中进行初始化的私有成员可以声明为构造器函数的局部变量,就像这样:

例附录2.21:

要注意的是,这样的私有成员同我们所认为的C#中的私有成员稍有不同。在C#中,类的公开方法可以直接访问类的私有成员。但是在JavaScript中,私有成员只有通过在闭包中包含有这些私有成员的方法来访问(这样的方法通常称为特权方法,因为它们不同于普通的公开方法)。因此,在Person的公开方法中,你依然可以通过Person的特权方法来访问私有成员:

例附录2.22:

免责声明:以上内容源自网络,版权归原作者所有,如有侵犯您的原创版权请告知,我们将尽快删除相关内容。

我要反馈