在Javascript面向对象编程中,原型继承不只是一个重点也是一个不容易掌握的点。在本文中,咱们将对Javascript中的原型继承进行一些探索。编程
咱们先来看下面一段代码:数组
<code>//构造器函数 function Shape(){ this.x = 0; this.y = 0; } //一个shape实例 var s = new Shape(); </code>
虽然这个例子很是简单,可是有四个“很是重要”的点须要在此阐明:函数
1.s是一个对象,而且默认的它拥有访问Shape.prototype(即每一个由Shape构造函数建立的对象拥有的原型)的权限;简单来讲,Shape.prototype就是一个“监视”着全部Shape实例的对象。你能够将一个对象的原型想象成一个由许多属性(变量/函数)组成的后备集合,当原型在它本身身上找不到东西时就会去原型中查找。this
2.原型能够在全部的Shape实例中共享。例如,全部的原型都拥有(直接)访问原型的权限。spa
3.当你调用实例中的一个函数时,这个实例会在它本身身上查找这个函数的定义。若是找不到,那么原型将会查找这个函数的定义。prototype
4.不管被调用的函数的定义在哪里找到(在实例自己中或者它的原型中),this的值都是指向用来调用函数的这个实例。所以若是咱们调用了一个s中干的函数,若是这个函数并无在s中直接定义而是在s的原型中,this值依然指向s。code
如今咱们将上面强调的几点运用到一个例子中。假设咱们将一个函数getPosition()绑定到s上。咱们可能会这样作:对象
<code>s.getPosition(){ return [this.x,this.y]; } </code>
这样作没有什么错误。你能够直接调用s.getPosition()而后你将得到返回的数组。继承
可是若是咱们建立了另外一个Shape的实例s2怎么办;它依然可以调用getPosition()函数吗?ip
答案显然是不能。
getPosition函数直接在实例s中北建立。所以,这个函数并不会讯在与s2中。
当你调用s2.getPosition()时,下面的步骤会依次发生(注意第三步很是重要):
1.实例s2会检查getPosition的定义;
2.这个函数不存在于s2中;
3.s2的原型(和s一块儿共享的后备集合)检查getPosition的定义;
4.这个函数不存在与原型中;
5.这个函数的定义没有被找到;
一个简单(但并非最优)的解决方案是将getPosition在实例s2(以及后面每个须要getPosition的实例)中再定义一次。这是一个很很差的作法由于你在作无心义的复制代码的工做,并且在每一个实例中定义一个函数会消耗更多的内存(若是你关心这点的话)。
咱们有更好的办法。
咱们彻底能够达到全部实例共享getPosition函数的目的,不是在每一个实例中都定义getPosition,而是在构造器函数的原型中。咱们来看下面的代码:
<code>//构造器函数 function Shape(){ this.x = 0; this.y = 0 ; } Shape.prototype.getPosition = function(){ return [this.x,this.y]; } var s = new Shape(), s2 = new Shape(); </code>
因为原型在全部Shape的实例中共享,s和s2都可以访问到getPosition函数。
调用s2.getPosition()函数会经历下面的步骤:
1.实例s2检查getPosition的定义;
2.函数不存在与s2中;
3.检查原型;
4.getPosition的定义存在于原型中;
5.getPosition会连同指向s2的this一块儿执行;
绑定到原型的属性很是适合于重用。你能够在全部的实例中重用一样的函数。
当你把对象或者数组绑定到原型中的时候要很是当心。全部的实例将会共享这些被绑定的对象/数组的引用。若是一个实例操纵了对象或数组,那么全部的实例都会受到影响。
<code>function Shape() { this.x = 0; this.y = 0; } Shape.prototype.types = ['round', 'flat']; s = new Shape(); s2 = new Shape(); s.types.push('bumpy'); console.log(s.types); // ['round', 'flat', 'bumpy'] console.log(s2.types); // ['round', 'flat', 'bumpy'] </code>
当s.types.push(‘bumpy’)这行代码运行时,实例s将会检查一个叫作types的数组。它不存在与实例s中,因而原型检查这个数组。这个数组,types,存在于原型中,所以咱们为他添加一个元素’bumpy’。
结果,因为s2也共享原型,它也能经过非直接的方式发现types数组发生了变化。
现实世界中当你使用Backbone.js时也会发生相似的事情。当你定义了一个视图/模型/集合,Backbone会把你经过extend函数(例如:Backbone.View.extend({}))传递的属性添加到你定义的实体的原型中。
这意味着若是你在定义实体时添加了一个对象或者数组,全部的实例将会共享这些对象或者数组,颇有可能你的一个实例会毁掉另一个实例。为了不这样的状况,你常常会看到任梦将这些对象/数组包括在一个函数中,每次返回一个对象/数组的实例:
注意:Backbone在model defaults的部分中谈到了这一点:
记住在Javascript中,对象是以引用的方式被传递的,所以若是你包含了一个对象做为默认值,它将在全部实例中被共享。所以,咱们将defaults定义为一个函数。
假设如今咱们想要建立一种特定类型的Shape,好比说一个圆。若是它能继承Shape的全部功能而且还能在它的原型中定义自定义函数那该多好:
<code>function Shape() { this.x = 0; this.y = 0; } function Circle() { this.radius = 0; } </code>
那么咱们怎么形容一个circle是一个shape呢?有如下几种方法:
当咱们建立一个圆时,咱们想要让实例拥有一个半径(来源于Circle构造函数),以及一个x位置,一个y位置(来源于Shape构造函数)。
咱们咱们仅仅声明c = new Circle(),那么c仅仅只有半径。Shape构造函数对x和y进行了初始化。咱们想要这个功能。所以咱们来借用这个功能。
<code>function Circle() { this.radius = 0; Shape.call(this); } </code>
最后一行代码Shape.call(this)调用了Shape构造函数并改变了当Circle构造函数被调用时指向this的this值。这是在说些什么?
如今咱们来使用上面的构造函数建立一个新的圆而后看看发生了什么:
<code>var c = new Circle(); </code>
这行代码调用了Circle构造函数,它首先在c上绑定了一个变量radius。记住,此时的this指向的是c。咱们接着调用Shape构造函数,而后将Shape中的this值指向当前在Circle中的this值,也就是c。Shape构造函数将x和y绑定到了当前的this上,也就是说,c如今拥有值为0的x和y属性。
另外,你在这个例子中放置Shape.call(this)的为止并不重要。若是你想在初始化以后重载x和y(也就是将圆心放在一个另外的地方),你能够在调用Shape函数以后完成这件事。
问题是如今咱们实例化的圆虽然拥有了变量x,y和radius,可是它并不能从Shape的原型中获取任何东西。咱们须要设置Circle构造函数来将Shape的原型重用为它的原型 — 以便全部的圆都能获取做为shape的福利。
一种方式是咱们将Circle.prototype的值设置为Shape.prototype:
<code>function Shape() { this.x = 0; this.y = 0; } Shape.prototype.getPosition = function () { return [this.x, this.y]; }; function Circle() { this.radius = 0; Shape.call(this); } Circle.prototype = Shape.prototype; var s = new Shape(), c = new Circle(); </code>
这样作运行的很好,可是它并非最优选择。实例c如今拥有访问getPosition函数的权限,由于Circle构造器函数和Shape构造器函数共享了它的原型。
要是咱们还想给全部元定义一个getArea函数怎么办?咱们将把这个函数绑定到Circle构造器函数的原型中以便它能够为全部圆所用。
编写下面的代码:
<code>function Shape() { this.x = 0; this.y = 0; } Shape.prototype.getPosition = function () { return [this.x, this.y]; }; function Circle() { this.radius = 0; Shape.call(this); } Circle.prototype = Shape.prototype; Circle.prototype.getArea = function () { return Math.PI * this.radius * this.radius; }; var s = new Shape(), c = new Circle(); </code>
如今的状况是Circle和Shape共享同一个原型,咱们在Circle.prototype中添加了一个函数其实也就至关于在Shape.prototype中添加了一个函数。
怎么会这个样子!
一个Shape的实例并无radius变量,只有Circle实例拥有radius变量。可是如今,全部的Shape实例均可以访问getArea函数 — 这将致使一个错误,可是当全部圆调用这个函数时则一切正常。
将全部的原型设置为同一个对象并不能知足咱们的需求。
<code>function Shape() { this.x = 0; this.y = 0; } Shape.prototype.getPosition = function () { return [this.x, this.y]; }; function Circle() { this.radius = 0; } Circle.prototype = new Shape(); var c = new Circle(); </code>
这个方法很是的酷。咱们并无借用构造器函数可是Circle拥有了x和y,同时也拥有了getPosition函数。它是怎么实现的呢?
Circle.prototype如今是一个Shape的实例。这意味着c有一个直接的变量radius(由Circle构造器函数提供)。然而,在c的原型中,有一个x和y。如今注意,有趣的东西要来了:在c的原型的原型中,有一个getPosition函数的定义。看起来实际上是这样的:
所以,若是你试图获取c.x,那么它将在c的原型中被找到。
这种方法的缺点是若是你想要重载x和y,那么你必须在Circle构造器或者Circle原型中作这件事。
<code>function Shape() { this.x = 0; this.y = 0; } Shape.prototype.getPosition = function () { return [this.x, this.y]; }; function Circle() { this.radius = 0; } Circle.prototype = new Shape(); Circle.prototype.x = 5; Circle.prototype.y = 10; var c = new Circle(); console.log(c.getPosition()); // [5, 10] </code>
调用c.getPosition将会经历下列步骤:
1.该函数在c中没有被找到;
2.该函数在c的原型(Shape的实例)中没有被找到;
3.该函数在Shape实例的原型(c的原型的原型)中被找到;
4.该函数连同指向c的this一块儿被调用;
5.在getPosition函数的定义中,咱们在this中寻找x;
6.x没有直接在c中被找到;
7.咱们在c的原型(Shape实例)中查找x;
8.咱们在c的原型中找到x;
9.咱们在c的原型中找到y;
除了有一层一层的原型链带来的头痛以外,这个方法仍是很不错的。
这个方法还可使用Object.create()来替代。
<code>function Shape() { this.x = 0; this.y = 0; } Shape.prototype.getPosition = function () { return [this.x, this.y]; }; function Circle() { this.radius = 0; Shape.call(this); this.x = 5; this.y = 10; } Circle.prototype = Object.create(Shape.prototype); var c = new Circle(); console.log(c.getPosition()); // [5, 10] </code>
这个方法的一大好处就是x和y直接被绑定到了c上 — 这将使查询速度大大提升(若是你的程序关心这件事情)由于你不再须要向上查询原型链了。
咱们来看一看Object.create的替代方法(polyfill):
<code>Object.create = (function(){ // 中间构造函数 function F(){} return function(o){ ... // 将中间构造函数的原型设置为咱们给它的对象o F.prototype = o; // 返回一个中间构造函数的实例; // 它是一个空对象可是原型是咱们给它的对象o return new F(); }; })(); </code>
上说过程基本上是完成了Circle.prototype = new Shape();只是如今Circle.prototype是一个空对象(一个中间构造函数F的实例),而它的原型是Shape.prototype。
很是重要的一点是记住若是你在Shape构造函数上绑定有对象/数组,那么全部的圆均可以修改这些共享的对象/数组。若是将Circle.prototype设置为一个Shape的实例时这个方法会有很大的缺陷。
<code>function Shape() { this.x = 0; this.y = 0; this.types = ['flat', 'round']; } Shape.prototype.getPosition = function () { return [this.x, this.y]; }; function Circle() { this.radius = 0; } Circle.prototype = new Shape(); var c = new Circle(), c2 = new Circle(); c.types.push('bumpy'); console.log(c.types); // ["flat", "round", "bumpy"] console.log(c2.types); // ["flat", "round", "bumpy"] </code>
为了不这种状况的发生,你能够借用Shape的构造函数而且使用Object.create以便每个圆都能拥有它本身的types数组。
<code>... function Circle() { this.radius = 0; Shape.call(this); } Circle.prototype = Object.create(Shape.prototype); var c = new Circle(), c2 = new Circle(); c.types.push('bumpy'); console.log(c.types); // ["flat", "round", "bumpy"] console.log(c2.types); // ["flat", "round"] </code>
咱们如今在前面讨论的基础上更进一步,建立一个新的Circle的类型,Sphere。一个椭圆和圆差很少,只是在计算面积时有不一样的公式。
<code>function Shape() { this.x = 0; this.y = 0; } Shape.prototype.getPosition = function () { return [this.x, this.y]; }; function Circle() { this.radius = 0; Shape.call(this); this.x = 5; this.y = 10; } Circle.prototype = Object.create(Shape.prototype); Circle.prototype.getArea = function () { return Math.PI * this.radius * this.radius; }; function Sphere() { } // TODO: 在这里设置原型链 Sphere.prototype.getArea = function () { return 4 * Math.PI * this.radius * this.radius; }; var sp = new Sphere(); </code>
咱们应该使用哪一种方法来设置原型链?记住,咱们并不想要毁掉咱们关于圆的getArea的定义。咱们只是想在椭圆中有另外一种方式的实现。
咱们并可以借用构造函数并为原型赋值(方法1)。由于这样作将会改变全部圆的getArea的定义。然而,咱们可使用Object.create或者将Sphere的原型设置为一个Circle的实例。咱们来看看应该怎么作:
<code>... function Circle() { this.radius = 0; Shape.call(this); this.x = 5; this.y = 10; } Circle.prototype = Object.create(Shape.prototype); Circle.prototype.getArea = function () { return Math.PI * this.radius * this.radius; }; function Sphere() { Circle.call(this); } Sphere.prototype = Object.create(Circle.prototype); Sphere.prototype.getArea = function () { return 4 * Math.PI * this.radius * this.radius; }; var sp = new Sphere(); </code>
调用sp.getArea()将会经历一下步骤:
1.在sp中查找getArea的定义;
2.在sp中没有找到相关定义;
3.在Sphere的原型(一个中间对象,它的原型是Circle.prototype)中查找;
4.在这个中间对象中找到关于getArea的定义,因为咱们在Sphere的原型中从新定义了getArea,这里采用新的定义;
5.连同指向sp的this调用getArea方法;
咱们注意到Circle.prototype也有一个getArea的定义。然而,因为Sphere.prototype已经有了一个getArea的定义,咱们永远不会使用到Circle.prototype中的的getArea — 这样咱们就成功的“重载”了这个函数(重载一位这在查询链的前面定义了一个名字相同的函数)。