JavaScript作為一個面向對象語言,可以實現繼承是必不可少的,但是由於本身並沒有類的概念(不知道這樣說是否嚴謹,但在js中一切都類皆是對象模擬)所以在JavaScript中的繼承也區別於其他的面向對象語言。可能很多初學者知道實現js繼承的方法,但卻對實現繼承的原理一頭霧水。所以,今天我們就來圖解JavaScript繼承。(因為繼承有關於原型相關知識,所以希望大家對原型有一定的了解推薦閱讀:理解JavaScript原型 梳理JavaScript原型整體思路)。
下面我們就開始看看JavaScript各種繼承方式及他們的原理
1.默認模式
-
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的一個實例。
根據上圖可以看出,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 由此形成繼承。
默認模式的方式沒有繼承上級構造函數自身的屬性,只是可以通過原型鏈向上查找而使用它而已。如果繼承者為自己設置該屬性,則會屏蔽原型鏈上的其他同名屬性。
看一看上面代碼的輸出可以看出14、15行證明繼承而來的屬性並沒能在自身創造一個新的該屬性,只是通過原型向上查找的方式來獲取該屬性,正因為如此16~19行的輸出可以看出,kid對name屬性的更改會影響到父構造函數中的name屬性。
2.借用構造函數
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)。
從上圖可以很明顯看出Article與StaticPage並沒有連接,也就是說使用借用構造函數的方式,因為直接以修改調用位置的方法使用Article構造函數,所以繼承了Article內部的屬性,獨立創建出屬性,但是由於沒有使用StaticPage.prototype所以StaticPage會自動創建出一個空的prototype對象。所以StaticPage並沒有繼承到Article原型鏈上的方法。
在上面的例子代碼中有很多個輸出,現在我們來研究一下輸出那些答案的原因並借以證明上面的話~
首先15、16行判斷tags是不是article和page(注意這是兩個實例化出的對象)的自身屬性,返回值皆為true,由此可以說明StaticPage的確繼承了Article中添加到this的屬性。
17、18、19、20行中,在page沒有為tags專門賦值時可輸出父構造內部tags的值即 ['js', 'css'] 當賦值為 ['html', 'node'] 后page的tags值改變但article的值並沒有改變(20行),由此可見StaticPage繼承Article是獨立創造了其內部的屬性(因為是修改調用位置的方式,所以會創建新的屬性而不會產生關聯)。
21、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();
臨時構造函數的意思就是通過一個臨時的構造函數來實現繼承,正如上面代碼的11、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繼承的學習過程中有了很多對實現原理不理解的地方,導致我一直不能記住並且正確使用這部分知識,所以當我感覺自己對繼承有了一部分了解之后寫下了這篇博客,博客中的內容都是我個人的理解,如果有解釋不到位或理解有偏差的地方還請大神告知,小女子在此謝過~