在全部面向對象的編程中,繼承是一個重要的話題。通常說來,在設計類的時候,咱們但願能減小重複性的代碼,而且儘可能弱化對象間的耦合(讓一個類繼承另外一個類可能會致使兩者產生強耦合)。關於「解耦」是程序設計中另外一個重要的話題,本篇重點來看看在javascript如何實現繼承。javascript
其它的面向對象程序設計語言都是經過關鍵字來解決繼承的問題(好比extend或inherit等方式)。可是javascript中並無定義這種實現的機制,若是一個類須要繼承另外一個類,這個繼承過程須要程序員本身經過編碼來實現。java
一、建立一個類的方式:程序員
1 //定義類的構造函數 2 function Person(name) { 3 this.name = name || '默認姓名'; 4 } 5 //定義該類全部實例的公共方法 6 Person.prototype.getName = function() { 7 return this.name; 8 } 9 10 var smith = new Person('Smith'); 11 var jacky = new Person('Jacky'); 12 13 console.log( smith.getName(), jacky.getName() ); //Smith Jacky
二、繼承這個類:這須要分兩個步驟來實現,第1步是繼承父類構造函數中定義的屬性,第2步是繼承父類的prototype屬性編程
//定義類的構造函數 function Person(name) { this.name = name || '默認姓名'; } //定義該類全部實例的公共方法 Person.prototype.getName = function() { return this.name; } function Author(name, books) { //繼承父類構造函數中定義的屬性 //經過改變父類構造函數的執行上下文來繼承 Person.call(this, name); this.books = books; } //繼承父類對應的方法 Author.prototype = new Person(); //Author.prototype.constructor === Person Author.prototype.constructor = Author; //修正修改原型鏈時形成的constructor丟失 Author.prototype.getBooks = function() { return this.books; }; //測試 var smith = new Person('Smith'); var jacky = new Author('Jacky', ['BookA', 'BookB']); console.log(smith.getName()); //Smith console.log(jacky.getName()); //Jacky console.log(jacky.getBooks().join(', ')); //BookA, BookB console.log(smith.getBooks().join(', ')); //Uncaught TypeError: smith.getBooks is not a function
從測試的結果中能夠看出,Author正確繼承了Person,並且修改Author的原型時,並不會對Person產生影響。這其中的關鍵一句就是 Author.prototype = new Person(),要與Author.prototype = Person.prototype區分開來。前者產生了一個實例,這個實例有Person.prototype的副本(這裏先這麼理解,後面有更詳細的解析)。後者是指將二者的prototype指向同一個原型對象。函數
那麼,這也意味着每次繼承都將產生一個父類的副本,確定對內存產生消耗,但爲了類式繼承這個內存開銷必須得支付,但還能夠作得更節省一點:Author.prototype = new Person()這一句其實多執行了構造函數一次(而這一次其實只需在子類構造函數中執行便可),尤爲是在父類的構造函數很龐大時很耗時和內存。修改一下繼承的方式,以下:測試
Author.prototype = (function() { function F() {} F.prototype = Person.prototype; return new F(); })();
如上所示的代碼,new時,去掉了對父類的構造函數的調用,節省了一次調用的開銷。this
三、類式繼承顯著的特色是每一次實例化對象時,子類都將執行一次父類的構造函數。若是E繼承了D,D繼承了C,C繼承了B,B繼承了A,在實例化一個E時,一共要通過幾回構造函數的調用呢?編碼
/*繼承方法的函數*/ function extend(son, father) { function F() {} F.prototype = father.prototype; son.prototype = new F(); son.prototype.constructor = son; } //A類 function A() { console.log('A()'); } A.prototype.hello = function() { console.log('Hello, world.'); } //B類 function B() { A.call(this); console.log('B()'); } extend(B, A); //C類 function C() { B.call(this); console.log('C()'); } extend(C, B); //D類 function D() { C.call(this); console.log('D()'); } extend(D, C); //E類 function E() { D.call(this); console.log('E()'); } extend(E, D); //建立一個E的實例 var e = new E(); //A() B() C() D() E() e.hello(); //hello, world.
5次,這還只是實例化一個E時調用的次數。因此,咱們應該儘量的減小繼承的級別。但這並非說不要使用這種類式繼承,而是應該根據本身的應用場合決定採用什麼方法。spa
一、先來看一段代碼:咱們先將以前類式繼承中的繼承prototype那一段改爲另外一個函數clone,而後經過字面量建立一個Person,最後讓Author變成Person的克隆體。prototype
//這個函數能夠理解爲克隆一個對象 function clone(object) { function F() {} F.prototype = object; return new F(); } var Person = { name: 'Default Name'; getName: function() { return this.name; } } //接下來讓Author變爲Person的克隆體 var Author = clone(Person);
問一個問題:clone函數裏的new F()爲這個實例開闢內存空間來存儲object的副本了嗎?
按我以前的理解,回答是確定的。可是,當我繼續將代碼寫下去的時候,奇怪的事情發生了,代碼以下:
//接下來讓Author變爲Person的克隆體 var Author = clone(Person); Author.books = []; Author.getBooks = function() { return this.books.join(', '); } //增長一個做者Smith var Smith = clone(Author); console.log(Smith.getName(), Smith.getBooks()); //Default Name Smith.name = 'Smith'; Smith.books.push('<<Book A>>', '<<Book B>>'); //做者寫了兩本書 console.log(Smith.getName(), Smith.getBooks()); //Smith <<Book A>>, <<Book B>> //再增長一個做者Jacky var Jacky = clone(Author); console.log(Jacky.getName(), Jacky.getBooks()); // Default Name <<Book A>>, <<Book B>>
當咱們繼續增長做者Jacky時,奇怪的現象發生了!!Jacky的名字依然是Default Name,可是他竟然也寫兩本與Smith同樣的書?Jacky的書都還沒push呢。到了這裏,我想到了引用對象的狀況(引用一個對象時,引用的是該對象的內存地址),發生這樣的現象,問題確定出在clone()函數中的new F()這裏。
事實上,這個clone中的new F()確實返回了一個新的對象,該對象擁有被克隆對象的全部屬性。但這些屬性保留的是對被克隆對象中相應屬性的引用,而非一個徹底獨立的屬性副本。換句話說,新對象的屬性 與 被克隆的對象的屬性指向同一個內存地址(學過C語言的同窗應該明白指針類型,這裏意義差很少)。
那麼爲何上面的代碼中,Jacky的書與Smith的書相同了,爲何Jacky的名字卻不是Smith而是Default Name呢?這就是Javascript中繼承的機制所在,當Smith剛剛繼承自Author時,他的屬性保留了對Author的屬性的引用,一旦咱們顯示的對Smith的屬性從新賦值時,Javascritp引擎就會給Smith的該屬性從新劃份內存空間來存儲相應的值,因爲從新劃分了內址地址,那麼對Smith.name的改寫就不會影響到Author.name去了。這就很好的解釋了前面的那個問題——爲何Jacky的名字卻不是Smith而是Default Name。
二、基於原型繼承
經過前面的狀況分析,能夠看出基於原型繼承的方式更能節約內存(只有在須要時候纔開闢新的內存空間)。但要注意:基於原型繼承時,對象的屬性必定要從新賦值後(從新劃份內存)再去引用該屬性。對於對象的方法,若是有不一樣的處理方式,咱們只需從新定義便可。
下面將前一段代碼作一個完整、正確的範例出來,以說明原型繼承的特色和使用方式:
//這個函數能夠理解爲克隆一個對象 function clone(object) { function F() {} F.prototype = object; return new F(); } var Person = { name: 'Default Name', getName: function() { return this.name; } } //接下來讓Author變爲Person的克隆體 var Author = clone(Person); Author.books = []; Author.getBooks = function() { return this.books.join(', '); } //增長一個做者Smith var Smith = clone(Author); Smith.name = 'Smith'; Smith.books = []; Smith.books.push('<<Book A>>', '<<Book B>>'); //做者寫了兩本書 console.log(Smith.getName(), Smith.getBooks()); //Smith <<Book A>>, <<Book B>> //再增長一個做者Jacky var Jacky = clone(Author); Jacky.name = 'Jacky'; Jacky.books = []; Jacky.books.push('<<Book C>>', '<<Book D>>'); console.log(Jacky.getName(), Jacky.getBooks()); // Jacky <<Book C>>, <<Book D>>
一、類式繼承中:使用構造函數初始化對象的屬性,經過調用父類的構造函數來繼承這些屬性。經過new 父類的prototype來繼承方法。
二、原型式繼承中:去掉了構造函數,但須要將對象的屬性和方法寫一個{}裏申明。準確的說,原型式繼承就是類式繼承中繼承父類的prototype方法。