在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屬性被改變,改為指向父類的構造函數。所以為了不會導致繼承鏈的紊亂,我們需要手動把原型對象的構造函數指針重新指向自己。
好了終於寫完了,希望大家看得明白!\( ̄▽ ̄)/