JavaScript做爲一個面嚮對象語言,能夠實現繼承是必不可少的,可是因爲自己並無類的概念(不知道這樣說是否嚴謹,但在js中一切都類皆是對象模擬)因此在JavaScript中的繼承也區別於其餘的面嚮對象語言。可能不少初學者知道實現js繼承的方法,但卻對實現繼承的原理一頭霧水。因此,今天咱們就來圖解JavaScript繼承。(由於繼承有關於原型相關知識,因此但願你們對原型有必定的瞭解推薦閱讀:理解JavaScript原型 梳理JavaScript原型總體思路)。css
下面咱們就開始看看JavaScript各類繼承方式及他們的原理html
1.默認模式node
1 /** 2 * [默認繼承模式] 3 */ 4 function Parent(name) { 5 this.name = name || 'Adam'; 6 } 7 var parent = new Parent(); 8 Parent.prototype.say = function() { 9 return this.name; 10 }; 11 function Child(name) {} 12 Child.prototype = new Parent(); 13 var kid = new Child(); 14 console.log(kid.hasOwnProperty('name'));//false 15 console.log(parent.hasOwnProperty('name'));//true 16 console.log(kid.say());//Adam 17 kid.name = 'lili'; 18 console.log(kid.say());//lili 19 console.log(parent.say());//Adam
上面代碼實現繼承的是第12行的 Child.prototype = new Parent();經過這句代碼將Child的原型成爲Parent的一個實例。app
根據上圖能夠看出,Parent是一個構造函數,而且有一個Parent.prototype對象,對象內部有一個say()方法(此處不一一列舉prototype其餘那隻屬性方法)。又存在一個Child構造函數,咱們讓Parent實例化出的對象看成Cihld的prototype(畢竟prototype也是一個對象,因此無何不可)。其實爲了更方便理解,這句話能夠改爲 var parent1 = new Parent(); Child.prototype = parent1l; 這樣就比較顯而易見了。這樣賦值的結果就是:Child是Parent實例化出的對象,因此Child.__proto__是Parent.prototype。kid爲Cihld實例化出的對象,因此:kid.__proto__是Parent 創建原型鏈 kid--->Child--->Parent(Child.prototype)--->Parent.prototype 由此造成繼承。函數
默認模式的方式沒有繼承上級構造函數自身的屬性,只是能夠經過原型鏈向上查找而使用它而已。若是繼承者爲本身設置該屬性,則會屏蔽原型鏈上的其餘同名屬性。學習
看一看上面代碼的輸出能夠看出1四、15行證實繼承而來的屬性並沒能在自身創造一個新的該屬性,只是經過原型向上查找的方式來獲取該屬性,正由於如此16~19行的輸出能夠看出,kid對name屬性的更改會影響到父構造函數中的name屬性。this
2.借用構造函數spa
1 /** 2 * [借用構造函數] 3 */ 4 function Article(tags) { 5 this.tags = tags || ['js', 'css']; 6 } 7 Article.prototype.say = function() { 8 return this.tags 9 } 10 var article = new Article(); 11 function StaticPage(tags) { 12 Article.apply(this,arguments); 13 } 14 var page = new StaticPage(); 15 console.log(page.hasOwnProperty('tags'));//true 16 console.log(article.hasOwnProperty('tags'));//true 17 console.log(page.tags);//['js', 'css'] 18 page.tags = ['html', 'node']; 19 console.log(page.tags);//['html', 'node'] 20 console.log(article.tags);//['js', 'css'] 21 //console.log(page.say());//報錯 undefined is not a function 22 console.log(article.say());//['js', 'css']
上面代碼實現繼承的是第12行的Article.apply(this,arguments);經過這句代碼經過使用apply方法調用Article構造函數更改this指向(關於this:JavaScript中我很想說說的this)。prototype
從上圖能夠很明顯看出Article與StaticPage並無鏈接,也就是說使用借用構造函數的方式,由於直接以修改調用位置的方法使用Article構造函數,因此繼承了Article內部的屬性,獨立建立出屬性,可是因爲沒有使用StaticPage.prototype因此StaticPage會自動建立出一個空的prototype對象。因此StaticPage並無繼承到Article原型鏈上的方法。3d
在上面的例子代碼中有不少個輸出,如今咱們來研究一下輸出那些答案的緣由並藉以證實上面的話~
首先1五、16行判斷tags是否是article和page(注意這是兩個實例化出的對象)的自身屬性,返回值皆爲true,由此能夠說明StaticPage的確繼承了Article中添加到this的屬性。
1七、1八、1九、20行中,在page沒有爲tags專門賦值時可輸出父構造內部tags的值即 ['js', 'css'] 當賦值爲 ['html', 'node'] 後page的tags值改變但article的值並無改變(20行),因而可知StaticPage繼承Article是獨立創造了其內部的屬性(由於是修改調用位置的方式,因此會建立新的屬性而不會產生關聯)。
2一、22行調用say方法。報錯證實page並沒能繼承到Article.prototype上的方法。
3.借用和設置原型(組合繼承)
1 /** 2 * 借用和設置原型 3 */ 4 function Bird(name) { 5 this.name = name || 'Adam'; 6 } 7 Bird.prototype.say = function() { 8 return this.name; 9 }; 10 function CatWings(name) { 11 Bird.apply(this, arguments); 12 } 13 CatWings.prototype = new Bird(); 14 var bird = new CatWings("Patrick"); 15 console.log(bird.name);//Patrick 16 console.log(bird.say());//Patrick 17 delete bird.name; 18 console.log(bird.say());//Adam
借用和設置原型的方式是最經常使用的繼承模式,它是結合前面兩種模式,先借用構造函數再設置子構造函數的原型使其指向一個構造函數建立的新實例。
首先CatWings使用借用構造函數的方式建立新的示例bird這樣bird能夠獨立建立出name屬性而不用與父構造函數的name有關聯。再將CatWings.prototype賦給Bird的實例化對象,這樣又將這兩個構造函數鏈接在一塊兒是bird對象能夠訪問父構造函數原型鏈上的方法。
4.共享原型
1 /** 2 * 共享原型 3 */ 4 function A(name) { 5 this.name = name || 'Adam'; 6 } 7 A.prototype.say = function(){ 8 return this.name; 9 }; 10 function B() {} 11 B.prototype = A.prototype; 12 var b = new B(); 13 console.log(b.name); 14 b.name = 'lili'; 15 console.log(b.say());
上面代碼實現繼承的是第11行的B.prototype = A.prototype;經過這句代碼將B的原型更改成A的原型。
這種方法很簡單,沒有什麼太多須要解釋的,可是它的弊端也很大:它並不能繼承到父構造內部的屬性,並且也只是可使用父構造原型上的屬性方法,而且子對象更改原型鏈上的屬性或方法同時會影響到父元素~
5.臨時構造函數
1 /** 2 * 臨時構造函數 3 */ 4 function C(name) { 5 this.name = name || 'Adam'; 6 } 7 C.prototype.say = function() { 8 return this.name; 9 }; 10 function D() {} 11 var E = function() {}; 12 E.prototype = C.prototype; 13 D.prototype = new E();
臨時構造函數的意思就是經過一個臨時的構造函數來實現繼承,正如上面代碼的十一、12行。
這個圖可能畫的不是那麼易於理解,但咱們的重點放在D.prototype那個橢圓上,你就會發現上面一樣寫着 new E() 是的他就是一個E構造函數的實例,而E在整個繼承過程當中不會出現實際的用處,他的做用只是爲了對父構造和子構造作一個鏈接,因此被稱爲臨時構造函數。這樣作的優勢是什麼呢? 首先他能解決共享原型的最大弊端就是能夠同時更改同一個原型而且會影響到其餘人,但這種方法中,雖然E與C是共享原型,但D使用過默認繼承的方式繼承的原型,就沒有權限對C.prototype進行更改。
6.原型繼承
原型繼承與上述幾種繼承模式有着很大的區別,上面的繼承模式皆是模擬類的繼承模式,但原型繼承中並無類,因此是一種無類繼承模式。
1 /** 2 * [原型繼承] 3 * @type {Object} 4 */ 5 function object(proto) { 6 function F() {} 7 F.prototype = proto; 8 return new F(); 9 } 10 11 var person = { 12 name: 'nana', 13 friends: ['xiaoli', 'xiaoming'] 14 }; 15 16 var anotherPerson = object(person); 17 anotherPerson.friends.push('xiaohong'); 18 var yetAnotherPerson = object(person); 19 anotherPerson.friends.push('xiaogang'); 20 console.log(person.friends);//["xiaoli", "xiaoming", "xiaohong", "xiaogang"]
21 console.log(anotherPerson.__proto__)//Object {name: "nana", friends: Array[4]}
能夠看到上面5~9行object函數經過object函數咱們實現了原型繼承。而在整個代碼中,雖然實現了anotherPerson和yetAnotherPerson對person這個對象的繼承,但其中並無構造函數。
由上圖能夠看出,由於構造函數的原型本就是一個對象,如今將一個須要被繼承的對象設定爲一個構造函數F的原型,並用這個構造函數實例化出一個對象anotherPerson,這樣,這個對象anotherPerson就能夠經過原型鏈查找找到person這個對象,並使用他上面的屬性或者方法。
在ECMAScript5中,這種模式已經經過方法Object.create()來實現,也就是說,不須要推出與object()相相似的函數,他已經嵌在JavaScript語言之中。
因爲在我對JavaScript繼承的學習過程當中有了不少對實現原理不理解的地方,致使我一直不能記住而且正確使用這部分知識,因此當我感受本身對繼承有了一部分了解以後寫下了這篇博客,博客中的內容都是我我的的理解,若是有解釋不到位或理解有誤差的地方還請大神告知,小女子在此謝過~