在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 — 這樣咱們就成功的「重載」了這個函數(重載一位這在查詢鏈的前面定義了一個名字相同的函數)。