深入淺出理解Javascript原型概念以及繼承機制


在Javascript語言中,原型是一個經常被討論到但是有非常讓初學者不解的概念。那么,到底該怎么去給原型定義呢?不急,在了解是什么之前,我們不妨先來看下為什么。

Javascript最開始是網景公司的死直男工程師Brendan Eich負責開發。起初設計的意願非常簡單,網景公司在1994年發布了Navigator瀏覽器0.9版(歷史上第一個比較成熟的網絡瀏覽器),這時候需要一個網頁腳本語言,使得瀏覽器可以與網頁互動。Brendan Eich認為這種語言無需復雜,盡量簡單。然而Javascript里面都是對象,必須有一種機制,將所有對象聯系起來,這就需要設計一個繼承機制。為了維持Javascript的簡單,Eich拋棄了傳統面向對象語言中類的設計,而使用構造函數的方式去實現繼承。比如下面一個構造函數:

1 function Stark(name){
2  this.name = name;
3 }

咳咳,為什么是Stark?因為博主是個骨灰級的冰與火之歌NC粉(●′艸`)ヾ 好好好言歸正傳,那么當我們要創建一個新的Stark對象時,僅需調用一個new命令:

1 var branStark = new Stark('布蘭');
2 console.log(branStark .name); // 布蘭

然而這種繼承方式有個最大的缺點,便是無法實現數據和方法的共享。比如

 1 function Stark(name){
 2  this.name = name;
 3  this.words = 'Winter is coming. ';
 4 }
 5 var branStark = new Stark('布蘭');
 6 var aryaStark = new Stark('艾莉亞');
 7 Stark.words = 'Summer is coming.'
 8 
 9 console.log(branStark.words); //Winter is coming.
10 console.log(aryaStark.words); //Winter is coming.

可以看到,當Stark改變了家族族語words屬性,布蘭和艾莉亞的族語並沒有改變。這是因為他們都有自己的一個Stark屬性和方法副本。有時候這是好事,不會造成父子類數據上的混亂,但是有時候我們需要統一規划,使子類之間有數據的共享(大家一個家族的為毛不共用一個族語啊(╯‵□′)╯︵┻━┻ ),也節約資源上的開銷。因此,Eich提出了一個概念:原型(prototype)。他為構造函數設置一個prototype屬性。prototype屬性包含一個對象,所有實例對象需要共享的屬性和方法,都放在這個對象里面;那些不需要共享的屬性和方法,就放在構造函數里面。實例對象一旦創建,將自動引用prototype對象的屬性和方法。也就是說,實例對象的屬性和方法,分成兩種,一種是本地的,另一種是引用的。

 1 function Stark(name){
 2  this.name = name;
 3 }
 4 Stark.prototype.words = 'Winter is coming.';
 5 
 6 var branStark = new Stark('布蘭');
 7 var aryaStark = new Stark('艾莉亞');
 8 
 9 console.log(branStark.words); //Winter is coming.
10 console.log(aryaStark.words); //Winter is coming.
11 
12 Stark.prototype.words = 'Summer is coming.';
13 console.log(branStark.words); //Summer is coming.
14 console.log(aryaStark.words); //Summer is coming.

可以看到,當Stark改變了words屬性,布蘭和艾莉亞的words屬性也跟着改變了。這就實現了整齊划一~

通過以上介紹,大家應該對Javascript中的prototype概念有了比較基礎的了解。但是在實際應用中,從父類繼承創建一個子類,還是沒那么簡單。子類有時候希望擁有自己的構造函數,這時候上面例子整齊划一的方法就不適用了。那么該如何保證子類既能繼承父類的方法,又能保留自己的特色呢?別急,首先我們需要更深入一點,了解一下原型鏈(prototype chain)的概念。

簡單整理一下構造函數,原型和實例之間的關系:“每個構造函數都有一個原型對象,原型對象都包含一個指向構造函數的指針,而實例都包含一個指向原型對象的內部指針”。好,那我們看下以下代碼:

 1 function Stark (){
 2   this.words = 'Winter is coming.';  
 3 }
 4 function BranStark(){
 5   this.pet = 'Summer';  
 6 }
 7 function Summer(){
 8   this.favor = 'meat';
 9 }
10 BranStark.prototype = new Stark();
11 Summer.prototype = new BranStark();
12 
13 summer = new Summer();
14 
15 console.log(summer.favor);  //meat
16 console.log(summer.pet);  //Summer
17 console.log(summer.words);  //Winter is coming.

以上展示一個原型鏈繼承的效果。我們讓Summer的原型對象等於BranStark的實例,而BranStark的實例中包含一個指向BranStark原型對象的內部指針;而BranStark的原型對象又等於Stark的實例,Stark的實例中包含一個指向Stark原型對象的內部指針。這種關系可以層層遞進,形成一條實例與原型的鏈條,這就是原型鏈。我在網上找了個圖,大概是這樣:

在JS中實現繼承,大概有兩種思路:一個就是我們一開始所講的使用構造函數;另一個就是上面例子所講述的原型鏈繼承。但兩者各有利弊,構造函數繼承會造成資源的浪費,方法和數據難以復用;原型鏈繼承當有包含引用類型值的原型時,則容易造成數據上的混亂。請看下面例子:

 1 function Stark (){ }
 2 Stark.prototype.words =  ['Winter is coming'];
 3 function BranStark(){
 4  this.name = '布蘭';
 5 }
 6 function AryaStark(){
 7  this.name = '艾莉亞';
 8 }
 9 
10 BranStark.prototype = new Stark();
11 AryaStark.prototype = new Stark();
12 
13 aryaStark = new AryaStark();
14 console.log(aryaStark.words);  //['Winter is coming.']
15 aryaStark.words.push('Needle is good.');
16 console.log(aryaStark.words);  //['Winter is coming.', 'Needle is good.']
17 
18 branStark =  new BranStark();
19 console.log(branStark.words);  //['Winter is coming.', 'Meat is good.']

 可以看到當原型內包含引用類型值時,子類實例aryaStark對words屬性做出修改后,連其他子類也受到影響啦!Σ(  ̄д ̄;)艾莉亞你這個坑爹貨!!咳,回顧下我們一開始所得到的結論:在實現繼承的時候,所有實例對象需要共享的屬性和方法,可以放在prototype對象里面;那些不需要共享的屬性和方法,就放在構造函數里面。所以為了避免艾莉亞這種調皮孩子亂搗蛋,我們可以稍微做點改進!

 1 function Stark (){
 2   this.words = ['Winter is coming.'];  
 3 }
 4 function BranStark(){
 5  this.name = '布蘭';
 6 }
 7 function AryaStark(){
 8  this.name = '艾莉亞';
 9 }
10 
11 BranStark.prototype = new Stark();
12 BranStark.prototype.constructor = BranStark;
13 AryaStark.prototype = new Stark();
14 AryaStark.prototype.constructor = AryaStark;
15 
16 aryaStark = new AryaStark();
17 console.log(aryaStark.words);  //['Winter is coming.']
18 aryaStark.words.push('Needle is good.');
19 console.log(aryaStark.words);  //['Winter is coming.', 'Needle is good.']
20 
21 branStark =  new BranStark();
22 console.log(branStark.words);  //['Winter is coming.']

 _(:з」∠)_於是這樣, 把需要被保護的數據放在構造函數就OK了,子類之間既能共享數據又能保證安全。這種繼承方式也叫組合繼承,是JS最常見的一種。注意到我增加了兩行代碼:

1 BranStark.prototype.constructor = BranStark;
2 AryaStark.prototype.constructor = AryaStark;

解釋下為什么:當我們把一個構造函數的prototype對象賦值給一個實例,我們相當於把該prototype對象完全刪除,賦予一個新值。而每一個prototype對象都有一個constructor屬性,指向它的構造函數。重新賦值后,constructor屬性被改變,改為指向父類的構造函數。所以為了不會導致繼承鏈的紊亂,我們需要手動把原型對象的構造函數指針重新指向自己。

好了終於寫完了,希望大家看得明白!\( ̄▽ ̄)/


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM