JavaScript

JavaScript 原型

JavaScript的对象模型非常强大,但它与标准面向对象语言的对象模型稍有不同。JavaScript采用的不是基于类的面向对象系统,而是更强大的原型模型,其中的对象可继承和扩展其他对象的行为。

如果你以前使用过Java、C++或其他任何基于传统面向对象编程的语言,咱们有必要谈一谈。

如果你没有使用过,而且有个约会,也请坐下来慢慢听我讲,说不定也能学到点东西。

跟你直说吧:JavaScript没有传统的面向对象模型,即从类创建对象的模型。事实上,JavaScript根本就没有类。在JavaScript中,对象从其他对象那里继承行为,我们称之为原型式继承(prototypal inheritance)或基于原型的继承。如果你接受过面向对象编程训练,可能对JavaScript多有抱怨,也深感困惑,但别忘了,相比于经典面向对象语言,基于原型的语言更通用。它们更灵活,效率更高,表达力更强——只要你愿意,就可使用JavaScript来实现经典继承。因此,如果你接受过经典面向对象编程训练,请坐下来,放松心情,打开心扉,为学习一些不同的东西作好准备。如果你对“经典面向对象编程”毫无概念,就说明你是一张白纸,这通常是天大的好事。

先来介绍一种更好的对象图


原型是什么


JavaScript对象可从其他对象那里继承属性和行为。更具体地说,JavaScript使用原型式继承,其中其行为被继承的对象称为原型。这旨在继承既有属性(包括方法),同时在新对象中添加属性。这说得过于抽象,我们来看一个示例。我们从用于创建小狗对象的原型开始,它可能类似于下面这样。

有了不错的小狗原型后,便可创建从该原型继承属性的小狗对象了。对于这些小狗对象,还可根据其具体需求添加属性和行为。例如,对于每个小狗对象,我们都将添加属性name、 breed和weight。这些小狗对象需要发出叫声、奔跑或摇尾巴时,都可使用原型提供的这些行为,因为它们从原型那里继承了这些行为。为了让你明白其中的工作原理,下面来创建几个小狗对象。

继承原型


首先,需要创建小狗对象Fido、 Fluffy和Spot的对象图,让它们继承新创建的小狗原型。为表示继承关系,我们将绘制从小狗实例到原型的虚线。别忘了,我们只将所有小狗都需要的方法和属性放在小狗原型中,因为所有小狗都将继承它们。对于所有随小狗对象而异的属性,如name,我们都将其都放在小狗实例中,因为每条小狗的这些属性都各不相同。

继承的工作原理


既然方法bark并不包含在各个小狗对象中,而是包含在原型中,如何让小狗发出叫声呢?这正是继承的用武之地。对对象调用方法时,如果在对象中找不到,将在原型中查找它,如下所示。

属性的情况也一样。如果我们编写了需要获取fido.name的代码,将从fido对象中获取这个值。如果要获取fido.species的值,将首先在对象fido中查找;在这里找不到后,将接着在小狗原型中查找(结果是找到了)。

重写原型


继承原型并不意味着必须与它完全相同。在任何情况下,都可重写原型的属性和方法,为此只需在对象实例中提供它们即可。这之所以可行,是因为JavaScript总是先在对象实例(即具体的小狗对象)中查找属性;如果找不到,再在原型中查找。因此,要为对象spot定制方法bark,只需在其中包含自定义的方法bark。这样,JavaScript查找方法bark以便调用它时,将在对象spot中找到它,而不用劳神去原型中查找。

下面来看看如何在对象spot中重写方法bark,让它发出叫声时显示says WOOF!。

原型从哪里来


前面花了很多篇幅讨论小狗原型,你现在可能想看的是代码示例,而不是对象图示例。那么,如何创建或获取小狗原型呢?实际上,你已经有了一个这样的原型,只是你没有意识到而已。

下面演示了如何在代码中访问这个原型:

属性prototype?

如何设置原型


前面说过,可通过构造函数Dog的属性prototype来访问原型对象,但这个原型对象包含哪些属性和方法呢?默认包含的不多。换句话说,你需要给原型添加属性和方法,这通常是在使用构造函数前进行的。

下面来设置小狗原型。为此,得有一个可供使用的构造函数。下面来看看如何根据对象图创建这样的构造函数:

创建构造函数后,便可以设置小狗原型了。我们希望它包含属性species以及方法bark、 run和wag,如下所示:

创建几个小狗对象并对原型进行测试


为测试这个原型,请在一个文件(dog.html)中输入下面的代码,再在浏览器中加载它。这里再次列出了前一页的代码,并添加了一些测试代码。请确保所有的小狗对象都像预期的那样发出叫声、奔跑和摇尾。

编写让Spot发出叫声时显示says WOOF!的代码

别担心,我们可没忘记Spot。Spot要求在发出叫声时显示says WOOF!,因此我们需要重写原型,给Spot提供自定义方法bark。下面来修改代码:

我有点疑惑,鉴于方法bark位于原型而不是对象中,其中的this.name怎么不会导致问题呢?

问得好。在没有使用原型的情况下,这很容易解释,因为this指的是方法被调用的对象。调用原型中的方法bark时,你可能认为this指的是原型对象,但情况并非如此。调用对象的方法时,this被设置为方法被调用的对象。即便在该对象中没有找到调用的方法,而是在原型中找到了它,也不会修改this的值。在任何情况

下,this都指向原始对象,即方法被调用的对象,即便该方法位于原型中亦如此。因此,即便方法bark位于原型中,调用这个方法时,this也将被设置为原始小狗对象,得到的结果也是我们期望的,如显示Fluffy says Woof!。

让所有的小狗都学会新技能


该让所有小狗都学会新技能了。没错,就是所有的小狗。使用原型后,如果给原型添加一个方法,所有的小狗对象都将立即从原型那里继承这个方法并自动获得这种新行为,包括添加方法前已创建的小狗对象。

假设我们要让所有小狗都会坐下,只需在原型中添加一个坐下的方法即可。

下面来让小狗Barnaby坐下:

深入研究

原型是动态的


Barnaby能够坐下了,看到这一点我们很高兴。实际上,现在所有的小狗都能够坐下,因为在原型中添加方法后,继承该原型的任何对象都说,情况亦如此。能使用这个方法。 当然,对属性来说,情况亦如此。

问:也就是说,给原型添加新的方法或属性后,继承该原型的所有对象实例都将立即看到它?

答: 如果你说的“看到”是继承的意思,那你说的完全正确。请注意,这提供了一个途径,让你只需在运行阶段修改原型,就可扩展或修改其所有实例的行为。

问: 我知道,给原型添加新属性后,继承该原型的所有对象都将包含这个属性,但修改原型的既有属性呢?这是否也会影响继承原型的所有对象?比方说,如果我将属性pecies的值从Canine改为Feline,会不会导致所有既有小狗对象的属性species都变成Feline

答: 是的。修改原型的任何属性时,都将影响继承该原型的所有对象——只要它们没有重写这个属性。

方法sit更有趣的实现


下面来让方法sit更有趣些:小狗开始处于非坐着(即站立)状态。在方法sit中,判断小狗是否是坐着的。如果不是,就让它坐着;如果是,就告诉用户小狗已经是坐着的。为此,需要一个额外的属性sitting,用于跟踪小狗是否是坐着的。下面来编写这样的代码:

这些代码的有趣之处在于,小狗实例刚创建时,从原型那里继承了属性sitting,该属性的值默认为false;但调用方法sit后,就给小狗实例添加了属性sitting的值,导致在小狗实例中创建了属性sitting。这让我们能够给所有小狗对象指定默认值,并在需要时对各个小狗进行定制。

测试新的sit方法

下面来尝试使用这个方法。请在你的代码中添加上述新属性sitting以及方法sit的新实现,再对代码进行测试。你将发现,现在可以让barnaby坐下,再让spot坐下,且每个小狗对象都独立地跟踪自己是否是坐着的。

再谈属性sitting的工作原理

下面来确保你明白了其中的工作原理,因为如果你没有仔细分析前述实现,可能遗漏重要的细节。要点如下:首次获取sitting的值时,是从原型中获取的;但接下来将sitting设置为true时,是在对象实例而不是原型中进行的。在对象实例中添加这个属性后,接下来每次获取sitting的值时,都将从对象实例中获取,因为它重写了原型中的这个属性。下面再次详细地介绍这一点。

既然说到属性,在代码中是否有办法判断使用的属性包含在实例还是原型中呢?

有办法,可使用每个对象都有的方法hasOwnProperty。如果属性是在对象实例中定义的,这个方法将返回true。如果属性不是在对象实例中定义的,但能够访问它,就可认为它肯定是在原型中定义的。

下面来对fido和spot调用这个方法。首先,我们知道,在小狗原型中定义了属性species,而且spot和fido都没有重写这个属性。因此,如果我们对这两个对象调用方法hasOwnProperty,并以字符串的方式传入属性名 species ,结果都将为false:

下面来尝试对属性sitting进行这种判断。我们知道,在原型中定义了属性sitting,并将其初始化为false,因此将spot.sitting设置为true时,将重写原型中的属性sitting,并在实例spot中定义属性sitting。下面来询问spot和fido自己是否定义了属性sitting:

建立原型链


咱们来考虑如何建立原型链。对象不仅可以继承一个原型的属性,还可继承一个原型链。基于前面考虑问题的方式,这并不难理解。假设我们需要一个用于创建表演犬的表演犬原型,并希望这个原型依赖于小狗原型提供的方法bark、 run和wag。下面就来建立这样的原型链,体会一下其中的各个部分是如何协同工作的。

原型链中的继承原理


为表演犬建立原型链后,下面来看看其中的继承原理。对于本页下方的每个属性和方法,请沿原型链向上找出它们都是在哪里定义的。

创建表演犬原型


创建小狗原型时,只需直接使用构造函数Dog的属性prototype提供的空对象,在其中添加要让每个小狗实例都继承的属性和方法即可。但创建表演犬原型时,我们必须做更多的工作,因为我们需要的是一个继承另一个原型(小狗原型)的原型对象。为此,我们必须创建一个继承小狗原型的对象,再亲自动手建立关联。当前,我们有一个小狗原型,还有一系列继承这个原型的小狗实例,而目标是创建一个继承小狗原型的表演犬原型以及一系列继承表演犬原型的表演犬实例。

为此,需要一步一步来完成。

首先,需要一个继承小狗原型的对象

前面说过,表演犬原型是一个继承小狗原型的对象。要创建继承小狗原型的对象,最佳方式是什么呢?其实就是前面创建小狗实例时一直采用的方式。你可能还记得,这种方式类似于下面这样:

上述代码创建一个继承小狗原型的对象,因为它与以前创建小狗实例时使用的代码完全相同,只是没有向构造函数提供任何实参。为什么这样做呢?因为在这里,我们只需要一个继承小狗原型的小狗对象,而不关心其细节。

当前,我们需要的是一个表演犬原型。与其他小狗实例一样,它也是一个继承小狗原型的对象。下面来看看如何将这个空的小狗实例变成所需的表演犬原型。

接下来,将新建的小狗实例变成表演犬原型

至此,我们有了一个小狗实例,但如何使其成为表演犬原型呢?为此,只需将它赋给构造函数ShowDog的属性prototype。等等,我们还没有构造函数ShowDog呢,下面就来创建它:

有了这样的构造函数后,便可将其属性prototype设置为一个新的小狗实例了:

来看看我们到了哪一步:我们有构造函数ShowDog,可用来创建表演犬实例。我们还

有一个表演犬原型,它是一个小狗实例。下面来将对象图中的标签“Dog”改为“表演犬原型”,确保它准确地反映了这些对象扮演的角色。但别忘了,表演犬原型依然是一个小狗实例。

有了构造函数ShowDog和表演犬原型后,我们需要回过头去补充一些细节。我们将深入研究这个构造函数,并给表演犬原型添加一些属性和方法,让表演犬具备所需的额外行为。

该补全原型了

我们设置了表演犬原型,但当前它只是一个空的小狗实例。现在该给它添加属性和行为,让它更像表演犬原型了。

要给表演犬添加的属性和方法如下:

创建表演犬实例

测试表演犬

请将前一页的所有代码以及下面的测试代码添加到一个网页中,对scotty进行详尽的测试。另外,顺便创建几个表演犬实例,并对他们进行测试。

scotty.stack();
scotty.bark();
console.log(scotty.league);
console.log(scotty.species);

问: 前面调用构造函数Dog来创建用作表演犬原型的小狗实例时,没有指定任何实参。这是为什么?

答: 因为对于这个小狗实例,我们唯一的要求是它继承了小狗原型。这个小狗实例不像Fido和Fluffy那样是

具体的小狗,而只是一个继承小狗原型的通用小狗实例。另外,所有继承表演犬原型的小狗都定义了自己的属性name、breed和weight。因此即便用作表演犬原型的小狗实例给这些属性设置了值,我们也根本看不到这些值,因为表演犬实例总是重写这些属性。

问: 那么,在用作表演犬原型的小狗实例中,这些属性是怎么样的呢?

答: 根本没有给它们赋值,因此它们都是未定义的。

问: 如果没有将ShowDog的属性prototype设置为一个小狗实例,结果将如何?

答: 所有的表演犬都不会有问题,但它们不会继承小狗原型的任何行为。这意味着它们不能发出叫声、奔

跑和摇尾,也不包含值为Canine的属性species。你可以自己试一试:把将ShowDog.prototype设置为new

Dog()的那行代码注释掉,再尝试让Scotty发出叫声。结果将如何呢?

小狗原型并非原型链的终点


前面介绍了两个原型链。第一个原型链包含从中派生出小狗对象的小狗原型;第二个原型链包含从中派生

出表演犬的表演犬原型,而表演犬原型又是从小狗原型派生出来的。

在这两个原型链中,终点都是小狗原型吗?实际上不是,因为小狗原型是从Object派生出来的。

事实上,你创建的每个原型链的终点都是Object。这是因为对于你创建的任何实例,其默认原型都是

Object,除非你对其进行了修改。

Object是什么

可将Object视为对象始祖,所有对象都是从它派生而来的。Object实现了多个重要的方法,它们是JavaScript对象系统的核心部分。在日常工作中,这些方法中的很多你都不会用到,但有几个经常会用到。你在本章前面就见到过其中一个:hasOwnProperty。

每个对象都继承了这个方法,因为归根结底,每个对象都是从Object派生而来的。别忘了,在本章前面,我们使用了方法hasOwnProperty来确定属性是在对象实例还是其原型中定义的。Object定义的另一个方法是toString,但实例通常会重写它。这个方法返回对象的字符串表示。稍后将演示如何重写这个方法,为对象提供更准确的描述。

作为原型的Object

你可能没有意识到,你创建的每个对象都有原型,该原型默认为Object。你可将对象的原型设置为其他对

象,就像我们对表演犬原型所做的那样,但所有原型链的终点都是Object。

充分发挥继承的威力之重写内置行为

继承内置对象时,可重写这些对象定义的方法。一种常见的情形是,重写Object定义的方法toString。所有对象都是从Object派生而来的,因此所有对象都可使用方法toString来获取其简单的字符串表示。例如,要在控制台中显示对象,可结合使用console.log和方法toString:

使用对象改善生活


学习JavaScript这样的复杂主题时,很容易只见树木,不见森林。但对JavaScript有了全面了解后,再回过头来研究整个森林将容易得多。学习JavaScript时,你每次都只学习其一个方面:基本类型(可随时像使用对象一样使用它们)、数组(它们有点像对象)、函数(真是奇怪,它们像对象一样包含属性和方法)、构造函数(既像对象又像函数),还有对象本身。这些看起来都非常复杂。掌握这些知识后,可以坐下来放松放松,深呼吸,回味“一切皆对象”的说法。

正如你看到的,一切皆对象。诚然,有一些基本类型,如布尔值、数字和字符串,但只要需要,你随时都可将它们视为对象。还有一些内置类型,如Date、 Math和RegEx,但它们也都是对象。即便数组也是对象。正如你看到的,它们之所以看起来不同,只是因为JavaScript提供了一些出色的语法糖,让我们能够更轻松地创建和访问对象。当然,还有对象本身,其中对象字面量简单易用,原型对象系统则提供了强大的功能。

函数呢?它们真的是对象吗?我们来验证这一点:

function meditate() {
console.log("Everything is an object...");
}
alert(meditate instanceof Object);

没错,函数确实是对象;但现在你不应觉得这有什么奇怪的。毕竟,我们可以将函数赋给变量(就像将对象赋给变量一样),将其作为实参传递给函数(就像对象一样),从函数返回它们(就像对象一样)。我们还发现,函数甚至包含属性,如下所示:

另外,你完全可以给函数添加新属性——如果这样做有所帮助的话。最后,顺便说一句,方法也是对象的一个属性,只是该属性被设置为一个匿名函数表达式而已。