JavaScript的对象模型非常强大,但它与标准面向对象语言的对象模型稍有不同。JavaScript采用的不是基于类的面向对象系统,而是更强大的原型模型,其中的对象可继承和扩展其他对象的行为。
JavaScript没有传统的面向对象模型,即从类创建对象的模型。事实上,JavaScript根本就没有类。在JavaScript中,对象从其他对象那里继承行为,我们称之为原型式继承(prototypal inheritance)或基于原型的继承。
tips: 这在ES6版本中发生变化: ES6中添加了类的概念。
再谈构造函数
我们来一看一下如下的构造函数:
function Dog(name, breed, weight) {
this.name = name;
this.breed = breed;
this.weight = weight;
this.bark = function () {
if (this.weight > 25) {
alert(this.name + " says Woof!");
} else {
alert(this.name + " says Yip!");
}
};
}
知识兔
知识兔通过使用这个构造函数,可创建一致的小狗对象,并根据喜好进行定制;还可利用这个构造函数中定义的方法(这里只有一个bark)。另外,每个小狗对象都从构造函数那里获得了相同的代码,未来需要修改代码时,这可避免很多麻烦。这很好,但在运行阶段执行下面的代码时,情况将如何呢?
var fido = new Dog("Fido", "Mixed", 38);
var fluffy = new Dog("Fluffy", "Poodle", 30);
var spot = new Dog("spot", "chihuahua", 10);
知识兔这些代码创建三个小狗对象。使用新对象图表示时,这些对象类似于下面这样。
从图中可以看出每个对象都有一个bark方法。
重复的方法真是个问题吗?
确实是个问题。一般而言,我们不希望每次使用构造函数实例化一个对象时,都创建一组新的方法。这样会影响应用程序的性能,占用计算机资源。这可能是个大问题,在移动设备上尤其如此。你将看到,还有更灵活、更强大的JavaScript对象创建方式。
我们回过头去想想使用构造函数的主要目的:试图重用行为。例如,创建大量的小狗对象时,我们希望这些对象都使用相同的bark方法。通过使用构造函数,我们只需将bark方法放在构造函数Dog中,这样每次实例化对象时,都将重用方法bark的代码,从而在代码层面实现重用行为的目的。但在运行阶段,这种解决方案的效果并不好,因为每个小狗对象都将获得自己的bark方法副本。
为何会出现这种问题呢?这是因为我们没有充分利用JavaScript的对象模型。JavaScript对象模型基于原型的概念,在这种模型中,可通过扩展其他对象(即原型对象)来创建对象。
为演示原型式继承,下面首先来创建小狗原型。
原型是什么?
JavaScript对象可从其他对象那里继承属性和行为。更具体地说,JavaScript使用原型式继承,其中其行为被继承的对象称为原型。这旨在继承既有属性(包括方法),同时在新对象中添加属性。(对象继承另一个对象后,便可访问其所有方法和属性。)
我们说一个(子)对象的原型(构造函数),也是一个(父)对象,子对象的实例对象可以继承父对象的属性和方法
来看一个示例:
我们从用于创建小狗对象的原型开始,它可能类似于下面这样。
有了不错的小狗原型后,便可创建从该原型继承属性的小狗对象了。对于这些小狗对象,还可根据其具体需求添加属性和行为。例如,对于每个小狗对象,我们都将添加属性name、breed和weight。(特别的)
这些小狗对象需要发出叫声、奔跑或摇尾巴时,都可使用原型提供的这些行为(共有的),因为它们从原型那里继承了这些行为。为了让你明白其中的工作原理,下面来创建几个小狗对象。
继承原型
首先,需要创建小狗对象Fido、Fluffy和Spot的对象图,让它们继承新创建的小狗原型。为表示继承关系,我们将绘制从小狗实例到原型的虚线。别忘了,我们只将所有小狗都需要的方法和属性放在小狗原型中,因为所有小狗都将继承它们。对于所有随小狗对象而异的属性,如name,我们都将其都放在小狗实例中,因为每条小狗的这些属性都各不相同。
继承的工作原理
既然方法bark并不包含在各个小狗对象中,而是包含在原型中,如何让小狗发出叫声呢?这正是继承的用武之地。对对象调用方法时,如果在对象中找不到,将在原型中查找它,如下所示。
属性的情况也一样。如果我们编写了需要获取fido.name的代码,将从fido对象中获取这个值。如果要获取fido.species的值,将首先在对象fido中查找;在这里找不到后,将接着在小狗原型中查找(结果是找到了)。
建立了这种继承关系,我们就可以创建大量的小狗对象,而且这些小狗对象在运行阶段使用同一个bark方法,从而避免了庞大的运行阶段开销。如下图所示:
明白如何使用继承后,便可以创建大量的小狗了。这些小狗都能发出叫声,但依赖于小狗原型提供的方法bark。
我们实现了代码重用:不仅只需在一个地方编写代码,而且让所有小狗实例都在运行阶段使用同一个bark方法,从
而避免了庞大的运行阶段开销。
你将看到,通过使用原型,可快速地创建对象,这些对象不仅能够重用代码,还能新增行为和属性。
重写原型
继承原型并不意味着必须与它完全相同。在任何情况 下,都可重写原型的属性和方法,为此只需在对象实例 中提供它们即可。这之所以可行,是因为JavaScript总 是先在对象实例(即具体的小狗对象)中查找属性;如 果找不到,再在原型中查找。因此,要为对象spot定 制方法bark,只需在其中包含自定义的方法bark。这 样,JavaScript查找方法bark以便调用它时,将在对象 spot中找到它,而不用劳神去原型中查找。
下面来看看如何在对象spot中重写方法bark,让它发出叫声时显示says WOOF!。
原型从哪里来
前面花了很多篇幅讨论小狗原型,你现在可能想看的是代码示例,而不是对象图示例。那么,如何创建或获取小狗原型呢?实际上,你已经有了一个这样的原型,只是你没有意识到而已。
下面演示了如何在代码中访问这个原型:
Dog.prototype //如果你查看构造函数Dog,将发现它有一个 prototype属性。这是一个指向原型的引用。
知识兔等等!!!
Dog是个构造函数,即函数。你是说它有属性吗?
是的,在JavaScript中,函数也是对象。实际上,在JavaScript中,几乎所有的东西都是对象,数组也
是——你可能还没有意识到这一点。
如何设置原型?
前面说过,可通过构造函数Dog的属性prototype来访问原型对象,但这个原型对象包含哪些属性和方法呢?默认包含的不多。换句话说,你需要给原型添加属性和方法,这通常是在使用构造函数前进行的。
下面来设置小狗原型。为此,得有一个可供使用的构造函数。下面来看看如何根据对象图创建这样的构造函数:
创建构造函数后,便可以设置小狗原型了。我们希望它包含属性species以及方法bark、run和wag,如下所示:
Dog.prototype.species = "Canine"; //将字符串"Canine"赋给原型的属性species。
Dog.prototype.bark = function () {
if (this.weight > 25) {
console.log(this.name + " says Woof!");
} else {
console.log(this.name + " says Yip!");
}
};
Dog.prototype.run = function () {
console.log("Run!");
};
Dog.prototype.wag = function () {
console.log("Wag!");
};
知识兔创建几个小狗对象并对原型进行测试
为测试这个原型,请在一个文件(index.html)中输入下面的代码,再在浏览器(chrome)中加载它。这里再次列出了前一页的代码,并添加了一些测试代码。请确保所有的小狗对象都像预期的那样发出叫声、奔跑和摇尾。
function Dog(name, breed, weight) {
this.name = name;
this.breed = breed;
this.weight = weight;
}
Dog.prototype.species = "Canine";
Dog.prototype.bark = function () {
if (this.weight > 25) {
console.log(this.name + " says Woof!");
} else {
console.log(this.name + " says Yip!");
}
};
Dog.prototype.run = function () {
console.log("Run!");
};
Dog.prototype.wag = function () {
console.log("Wag!");
};
var fido = new Dog("Fido", "Mixed", 38);
var fluffy = new Dog("Fluffy", "Poodle", 30);
var spot = new Dog("Spot", "Chihuahua", 10);
fido.bark();
fido.run();
fido.wag();
fluffy.bark();
fluffy.run();
fluffy.wag();
spot.bark();
spot.run();
spot.wag();
知识兔运行结果如下图所示:
编写让Spot发出叫声时显示says WOOF!的代码
别担心,我们可没忘记Spot。Spot要求在发出叫声时显示says WOOF!,因此我们需要重写原型,给
Spot提供自定义方法bark。下面来修改代码:
function Dog(name, breed, weight) {
this.name = name;
this.breed = breed;
this.weight = weight;
}
Dog.prototype.species = "Canine";
Dog.prototype.bark = function () {
if (this.weight > 25) {
console.log(this.name + " says Woof!");
} else {
console.log(this.name + " says Yip!");
}
};
Dog.prototype.run = function () {
console.log("Run!");
};
Dog.prototype.wag = function () {
console.log("Wag!");
};
var fido = new Dog("Fido", "Mixed", 38);
var fluffy = new Dog("Fluffy", "Poodle", 30);
var spot = new Dog("Spot", "Chihuahua", 10);
spot.bark = function() { //重写了原型的bark方法,该方法是spot自定义的
console.log(this.name + " says WOOF!");
};
fido.bark();
fido.run();
fido.wag();
fluffy.bark();
fluffy.run();
fluffy.wag();
spot.bark();
spot.run();
spot.wag();
知识兔疑惑:鉴于方法bark位于原型而不是对象中,其中的this.name怎么不会导致问题呢?
在没有使用原型的情况下,这很容易解释,因为this指的是方法被调用的对象。调用原型中的方法bark时,你可能认为this指的是原型对象,但情况并非如此。
调用对象的方法时,this被设置为方法被调用的对象。即便在该对象中没有找到调用的方法,而是在原型中找到了 它,也不会修改this的值。在任何情况下,this都指向原始对象,即方法被调用的对象,即便该方法位于原型中亦如此。因此,即便方法bark位于原型中, 调用这个方法时,this也将被设置为原始小狗对象,得到的结果也是我们期望的,如显示Fluffy says Woof!。
原型是动态的
让所有的小狗学会新技能
该让所有小狗都学会新技能了。没错,就是所有的小狗。使用原型后,如果给原型添加一个方法,所有的小狗对象都将立即从原型那里继承这个方法并自动获得这种新行为,包括添加方法前已创建的小狗对象。
假设我们要让所有小狗都会坐下,只需在原型中添加一个坐下的方法即可。
var barnaby = new Dog("Barnary", "Basset Hound", 55);
Dog.prototype.sit = function () {
console.log(this.name + " is now sitting!");
};
barnaby.sit();
spot.sit();
知识兔Barnaby能够坐下了,看到这一点我们很高兴。实际上,现在所有的小狗都能够坐下,因为在原型中添加方法后,继承该原型的任何对象都能使用这个方法。
疑问?
1.也就是说,给原型添加新的方法或属性后,继承该原型的所有对象实例都将立即看到它?
答: 如果你说的“看到”是继承的意思,那你说的完全正确。请注意,这提供了一个途径,让你只需在运行阶段
修改原型,就可扩展或修改其所有实例的行为。
2.我知道,给原型添加新属性后,继承该原型的所有对象都将包含这个属性,但修改原型的既有属性呢?这是
否也会影响继承原型的所有对象?比方说,如果我将属性species的值从Canine改为Feline,会不会导致所有既
有小狗对象的属性species都变成Feline?
答:是的。修改原型的任何属性时,都将影响继承该原型的所有对象--只要它们没有重写这个属性。
方法sit更有趣的实现
下面来让方法sit更有趣些:小狗开始处于非坐着(即站立)状态。在方法sit中,判断小狗是否是坐着的。如果不是,就让它坐着;如果是,就告诉用户小狗已经是坐着的。为此,需要一个额外的属性sitting,用于跟踪小狗是否是坐着的。下面来编写这样的代码:
这些代码的有趣之处在于,小狗实例刚创建时,从原型那里继承了属性sitting,该属性的值默认为false;但调用方法sit后,就给小狗实例添加了属性sitting的值,导致在小狗实例中创建了属性sitting。这让我们能够给所有小狗对象指定默认值,并在需要时对各个小狗进行定制。
再谈属性sitting的工作原理
下面来确保你明白了其中的工作原理,因为如果你没有仔细分析前述实现,可能遗漏重要的细节。要点如下:首次获取sitting的值时,是从原型中获取的;但接下来将sitting设置为true时,是在对象实例而不是原型中进行的。在对象实例中添加这个属性后,接下来每次获取sitting的值时,都将从对象实例中获取,因为它重写了原型中的这个属性。下面再次详细地介绍这一点。
既然说到属性,在代码中是否有办法判断使用的属性包含在实例还是原型中呢?
有办法,可使用每个对象都有的方法hasOwnProperty。如果属 性是在对象实例中定义的,这个方法将返回true。如果属性不是 在对象实例中定义的,但能够访问它,就可认为它肯定是在原型 中定义的。
下面来对fido和spot调用这个方法。首先,我们知道,在小狗 原型中定义了属性species,而且spot和fido都没有重写这个属性。因此,如果我们对这两个对象调用方法hasOwnProperty, 并以字符串的方式传入属性名 species ,结果都将为false:
spot.hasOwnProperty("species");
fido.hasOwnProperty("species");
知识兔上面这两条语句都返回false,因为species是在原型而不是对象实例spot和fido中定义的。
下面来尝试对属性sitting进行这种判断。我们知道,在原型中定义了属性sitting,并将其初始化为false,因此将spot.sitting设置为true时,将重写原型中的属性sitting,并在实例spot中定义属性sitting。下面来询问spot和fido自己是否定义了属性sitting:
spot.hasOwnProperty("sitting"); //首次检查spot是否有自己的sitting属性时,结果为false
spot.sitting = true; //接下来,我们将spot.sitting设 置为true,这将在实例spot中 添加属性sitting。
spot.hasOwnProperty("sitting"); //这次调用hasOwnProperty时,结 果为true,因为spot现在有自己 的sitting属性。
fido.hasOwnProperty("sitting"); //但对fido调用hasOwnProperty时,结果为false,因为实 例fido没有sitting属性。这意味着fido使用的sitting属性 是在原型中定义的,而fido从原型那里继承了这个属性。
知识兔建立原型链
使用JavaScript时,可以有多个原型。(就像你得到的遗产。你并非只继承了父母的特质,不是吗?你还继承了祖父母、外祖父母、曾祖父母、曾外祖父母等的一些特质。)在JavaScript中,原型还可以继承原型,可建立供对象继承的原型链。对象不仅可以继承一个原型的属性,还可继承一个原型链。基于前面考虑问题的方式,这并不难理解。
假设我们需要一个用于创建表演犬的表演犬原型,并希望这个原型依赖于小狗原型提供的方法bark、run和wag。下面就来建立这样的原型链,体会一下其中的各个部分是如何协同工作的。
原型链中的继承原理
为表演犬建立原型链后,下面来看看其中的继承原理。对于本页下方的每个属性和方法,请沿原型链向上找出它们都是在哪里定义的。
创建表演犬原型
创建小狗原型时,只需直接使用构造函数Dog的属性prototype提供的空对象,在其中添加要让每个小狗实例都继承的属性和方法即可。
但创建表演犬原型时,我们必须做更多的工作,因为我们需要的是一个继承另一个原型(小狗原型)的原型对象。为此,我们必须创建一个继承小狗原型的对象,再亲自动手建立关联。
当前,我们有一个小狗原型,还有一系列继承这个原型的小狗实例,而目标是创建一个继承小狗原型的表演犬原型以及一系列继承表演犬原型的表演犬实例。
为此,需要一步一步来完成。
首先,需要一个继承小狗原型的对象
前面说过,表演犬原型是一个继承小狗原型的对象。要创建继承小狗原型的对象,最佳方式是什么呢?其实就是前面创建小狗实例时一直采用的方式。你可能还记得,这种方式类似于下面这样:
上述代码创建一个继承小狗原型的对象,因为它与以前创建小狗实例时使用的代码完全相同,只是没有向构造函数提供任何实参。为什么这样做呢?因为在这里,我们只需要一个继承小狗原型的小狗对象,而不关心其细节。
当前,我们需要的是一个表演犬原型。与其他小狗实例一样,它也是一个继承小狗原型的对象。下面来看看如何将这个空的小狗实例变成所需的表演犬原型。
接下来,将新建的小狗实例变成表演犬原型
至此,我们有了一个小狗实例,但如何使其成为表演犬原型呢?为此,只需将它赋给构造函数ShowDog的属性prototype。等等,我们还没有构造函数ShowDog呢,下面就来创建它:
function ShowDog(name, breed, weight, handler) { //这个构造函数接受各种实参,用于设置小狗的属性 (name、breed、weight)和 表演犬的属性(handler)。
this.name = name;
this.breed = breed;
this.weight = weight;
this.handler = handler;
}
知识兔有了这样的构造函数后,便可将其属性prototype设置为一个新的小狗实例了:
ShowDog.prototype = new Dog(); //我们原本可以使用前一页创建的小狗实例,但为少使用一个变量,这里没有这样做,而是直接将 一个新小狗实例赋给属性prototype。
知识兔来看看我们到了哪一步:我们有构造函数ShowDog,可用来创建表演犬实例。我们还有一个表演犬原型,它是一个小狗实例。
下面来将对象图中的标签“Dog”改为“表演犬原型”,确保它准确地反映了这些对象扮演的角色。但别忘了,表演犬原型依然是一个小狗实例。
有了构造函数ShowDog和表演犬原型后,我们需要回过头去补充一些细节。我们将深 入研究这个构造函数,并给表演犬原型添加一些属性和方法,让表演犬具备所需的额外行为。
该补全原型了
我们设置了表演犬原型,但当前它只是一个空的小狗实例。现在该给它添加属性和行为,让它更像表演犬原型了。
要给表演犬添加的属性和方法如下:
创建表演犬实例
至此,我们只需做最后一件事:创建一个ShowDog实例。这个实例将从表演犬原型那里继承表演犬特有的属性和方法。另外,由于表演犬原型是一个小狗实例,这个表演犬也将从小狗原型那里继承所有的小狗行为和属性。因此它像其他小狗一样,也能够发出叫声、奔跑和摇尾。
下面列出了前面编写的所有代码,还有创建表演犬实例的代码:
function Dog(name, breed, weight) {
this.name = name;
this.breed = breed;
this.weight = weight;
}
Dog.prototype.species = "Canine";
Dog.prototype.bark = function () {
if (this.weight > 25) {
console.log(this.name + " says Woof!");
} else {
console.log(this.name + " says Yip!");
}
};
Dog.prototype.run = function () {
console.log("Run!");
};
Dog.prototype.wag = function () {
console.log("Wag!");
};
function ShowDog(name, breed, weight, handler) { //这个构造函数接受各种实参,用于设置小狗的属性 (name、breed、weight)和 表演犬的属性(handler)。
this.name = name;
this.breed = breed;
this.weight = weight;
this.handler = handler;
}
ShowDog.prototype = new Dog();
ShowDog.prototype.league = "Webville";
ShowDog.prototype.stack = function () {
console.log("Stack");
};
ShowDog.prototype.bait = function () {
console.log("Bait");
};
ShowDog.prototype.gait = function (kind) {
console.log(kind + "ing");
};
ShowDog.prototype.groom = function () {
console.log("Groom");
};
var scotty = new ShowDog("Scotty", "Scottish Terrier", 15, "Cookie");
scotty.bark();
scotty.stack();
console.log(scotty.league);
console.log(scotty.species);
知识兔最后的整理
我们来更深入地研究一下前面创建的各个小狗。前面对Fido进行了测试,发现它确实是小狗,下面来看看它是否也是表演犬(我们认为它不是)。Scotty呢?前面通过测试确定了它是表演犬,但它也是小狗吗?我们不确定。下面顺便来测试一下Fido和Scotty,看看它们都是使用哪个构造函数创建的。
var fido = new Dog("Fido", "Mixed", 38);
if (fido instanceof Dog) {
console.log("Fido is a Dog");
}
if (fido instanceof ShowDog) {
console.log("Fido is a ShowDog");
}
var scotty = new ShowDog("Scotty", "Scottish Terrier", 15, "Cookie");
if(scotty instanceof Dog) {
console.log("Scotty is a Dog");
}
if(scotty instanceof ShowDog) {
console.log("Scotty is a ShowDog");
}
console.log("Fido constructor is " + fido.constructor);
console.log("Scotty constructor is " + scotty.constructor);
知识兔通过浏览器控制台打印输出结果如下图所示:
想想为何会是这样的结果。首先,Fido显然只是小狗,而不是表演犬。事实 上,这完全符合我们的预期,毕竟Fido是使用构造函数Dog创建的,而这个构造函数与表演犬一点关系都没有。
接下来,Scotty既是小狗又是表演犬。这也合情合理,不过怎么会出现这样的结果呢?这是因为instanceof不仅考虑当前对象的类型,还考虑它继承的所有对象。Scotty虽然是作为表演犬创建的,但表演犬继承了小狗,因此Scotty也是小狗。
再接下来,Fido的构造函数为Dog。这合情合理,因为它就是使用这个构造函数创建的。
最后,Scotty的构造函数也是Dog。这不合理,因为它是使用构造函数ShowDog创建的。到底是怎么回事呢?先来看看这是如何得到的:查看属性scotty.constructor。由于我们没有显式地为表演犬设置这个属性,它将从小狗原型那里继承该属性。
为何会这样呢?坦率地说,这是一个需要修复的漏洞。正如你看到的,如果我们不显式地设置表演犬原型的属性constructor,就没人会这样做。不过,即便我们不这样做,一切也都将正常运行,但访问scotty.constructor时,
结果将不是预期的ShowDog,让人感到迷惑。
不用担心,下面就来修复这个问题。
首先,正如你看到的,没有正确地设置表演犬实例的属性constructor: 它们从小狗原型那里继承了这个属性。需要澄清的一点是,虽然代码都没问题,但给对象设置正确的构造函数是一种最佳实践,以免有一天另一 位开发人员接手这些代码并查看表演犬对象的情况时感到迷惑。
为修复属性constructor不正确的问题,需要在表演犬原型中正确地设置它。这样,创建表演犬实例时,它将继承正确的constructor属性,如下所示:
再次运行前面的测试,并核实表演犬实例Scotty的构造函数正确无误。
还有一个地方可以清理:构造函数ShowDog的代码。再来看一眼这个构造函数:
就这里而言,既然构造函数Dog已经知道如何完成这些工作,为何不让它去做呢?另外,虽然这个示例的代码很简单,但有些构造函数可能使用复杂的代码来计算属性的初始值。因此创建继承另一个原型的构造函数时,都不应重复既有的代码。下面来修复这个问题——先重写代码,再详细介绍它们:
正如你看到的,在构造函数ShowDog中,我们调用了方法Dog.call, 以此替换了那些重复的代码。这里的原理如下:call是一个内置方法,可对任何函数调用它(别忘了,Dog是一个函数)。Dog.call调用函 数Dog,将一个用作this的对象以及函数Dog的所有实参传递给它。下 面来详细介绍这行代码:
Dog.call详解
最终代码如下:
function Dog(name, breed, weight) {
this.name = name;
this.breed = breed;
this.weight = weight;
}
Dog.prototype.species = "Canine";
Dog.prototype.bark = function () {
if (this.weight > 25) {
console.log(this.name + " says Woof!");
} else {
console.log(this.name + " says Yip!");
}
};
Dog.prototype.run = function () {
console.log("Run!");
};
Dog.prototype.wag = function () {
console.log("Wag!");
};
function ShowDog(name, breed, weight, handler) {
Dog.call(this, name, breed, weight);
this.handler = handler;
}
ShowDog.prototype = new Dog();
ShowDog.prototype.constructor = ShowDog;
ShowDog.prototype.league = "Webville";
ShowDog.prototype.stack = function () {
console.log("Stack");
};
ShowDog.prototype.bait = function () {
console.log("Bait");
};
ShowDog.prototype.gait = function (kind) {
console.log(kind + "ing");
};
ShowDog.prototype.groom = function () {
console.log("Groom");
};
var fido = new Dog("Fido", "Mixed", 38);
var fluffy = new Dog("Fluffy", "Poodle", 30);
var spot = new Dog("Spot", "Chihuahua", 10);
var scotty = new ShowDog("Scotty", "Scottish Terrier", 15, "Cookie");
var beatrice = new ShowDog("Beatrice", "Pomeranian", 5, "Hamilton");
fido.bark();
fluffy.bark();
spot.bark();
scotty.bark();
beatrice.bark();
scotty.gait("Walk");
beatrice.groom();
知识兔Object是什么?
小狗原型并非原型链的终点
前面介绍了两个原型链。第一个原型链包含从中派生出小狗对象的小狗原型;第二个原型链包含从中派生出表演犬的表演犬原型,而表演犬原型又是从小狗原型派生出来的。
在这两个原型链中,终点都是小狗原型吗?实际上不是,因为小狗原型是从Object派生出来的。
事实上,你创建的每个原型链的终点都是Object。这是因为对于你创建的任何实例,其默认原型都是Object,除非你对其进行了修改。
可将Object视为对象始祖,所有对象都是从它派生而来的。Object实现了多个重要的方法,它们是JavaScript对
象系统的核心部分。在日常工作中,这些方法中的很多你都不会用到,但有几个经常会用到。
你在本章前面就见到过其中一个:hasOwnProperty。每个对象都继承了这个方法,因为归根结底,每个对象都是从Object派生而来的。别忘了,在本章前面,我们使用了方法hasOwnProperty来确定属性是在对象实例还是其原型中定义的。
Object定义的另一个方法是toString,但实例通常会重写它。这个方法返回对象的字符串表示。稍后将演示如何重写这个方法,为对象提供更准确的描述。
作为原型的Object
你可能没有意识到,你创建的每个对象都有原型,该原型默认为Object。你可将对象的原型设置为其他对象,就像我们对表演犬原型所做的那样,但所有原型链的终点都是Object。