在上一篇咱們講過關於原型對象的概念,固然若是不瞭解的建議去翻看第一篇文章,文末附有鏈接。咱們知道每一個對象都有各自的原型對象,那麼當咱們把一個對象的實例當作另一個對象的原型對象。。這樣這個對象就擁有了另一個引用類型的全部方法與屬性,當咱們再把該對象的實例賦予另外一個原型對象時,這樣又把這些方法繼承下去。如此層層遞進,對象與原型間存在連接關係,這樣就構成了原型鏈。app
function Animal(){ this.type = "Animal"; } Animal.prototype.say = function(){ console.log(this.type); } function Cat(){ this.vioce = "喵喵喵"; } Cat.prototype = new Animal(); Cat.prototype.shout = function(){ console.log(this.vioce); } let cat1 = new Cat(); cat1.say(); //"Animal" //固然,咱們還能夠繼續繼承下去 function Tom(){ this.name = "Tom"; } Tom.prototype = new Cat(); Tom.prototype.sayName = function(){ console.log(this.name); } let cat2 = new Tom(); cat2.say(); //"Animal" cat2.shout(); //"喵喵喵" cat2.sayName(); //"Tom" cat1.sayName(); //err 報錯表示沒有該函數
很神奇的,原型鏈就實現了對象的繼承。使用原型鏈就可使一個新對象擁有以前對象的全部方法和屬性。至於cat1.sayName()
會報錯,是由於該方法是在它的子原型對象中定義,因此沒法找到該函數。可是我相信不少人看到這裏仍是會一頭霧水,到底鏈在哪裏了?誰和誰鏈在一塊兒了?我用一張圖來讓你們更好的理解這個。函數
咋眼一看,這張圖信息量很多,可是理解起來卻一點都不難。咱們先從Animal
看起,Animal
中存在一個prototype
指向其原型對象,這一部分應該沒什麼問題。可是Animal
原型對象中卻存在[[prototype]]
指向了Object
,其實是指向了Object.prototype
。這是由於全部函數都是從Object繼承而來的,全部函數都是Object的實例。這也正是全部的函數均可以擁有Object方法的緣由,如toString()
。因此這也是原型鏈的一部分,咱們從建立自定義類型開始就已經踏入了原型鏈中。this
可是這部分咱們暫且無論它,咱們繼續往下面看。咱們把Animal
的實例當作Cat
的原型對象spa
Cat.prototype = new Animal();
這樣Cat
實例就擁有了其父類型的全部方法與屬性。由於代碼中尋找一個方法會不斷往上找,先在實例中尋找,若是沒有就在原型對象中去尋找,假如原型對象中沒有,就會往原型對象的原型對象中去找,如此遞進,最終若是找到則返回,找不到則報錯。當咱們構成原型鏈時,會有一個對象原型當作其父類型的實例,這樣便造成一條原型鏈。固然,若是如今有不明白 [[prototype]]
(__proto__
)與prototype
的區別能夠去翻看咱們第一篇文章,在這就不重複了。prototype
這樣一來咱們便明白了爲什麼cat1
中沒有sayName
函數並瞭解原型鏈如何實現繼承了。可是我又提出了一個問題,假如咱們把給子類型原型對象定義方法的位置調換一下,那麼會發生什麼事呢?code
function Animal(){ this.type = "Animal"; } Animal.prototype.say = function(){ console.log(this.type); } function Cat(){ this.vioce = "喵喵喵"; } Cat.prototype.shout = function(){ console.log(this.vioce); } Cat.prototype = new Animal(); let cat1 = new Cat(); cat1.say(); //"Animal" cat1.shuot(); //err,報錯無此函數
控制檯中會絕不留情的告訴你,沒有該方法Uncaught TypeError: cat1.shuot is not a function
。這是由於當你把父類的實例賦給子類原型對象時,會將其替換。那麼你以前所定義的方法就會失效。因此在這裏要注意的一點就是:給原型添加方法時必定要在替換原型語句以後,並且還有一點要注意就是,在用原型鏈實現繼承的時候,千萬不能夠用字面量形式定義原型方法。否則原型鏈會斷開。對象
function Animal(){ this.type = "Animal"; } Animal.prototype.say = function(){ console.log(this.type); } function Cat(){ this.vioce = "喵喵喵"; } Cat.prototype = new Animal(); Cat.prototype = { //這樣會使上一條語句失效,從而使原型鏈斷開。 shout:function(){ console.log(this.vioce); } }
接下來咱們談談原型鏈的問題。提及原型鏈的問題咱們大概能夠聯想到原型對象的問題:其屬性與方法會被全部實例共享,那麼在原型鏈中亦是如此。繼承
function Animal(){ this.type = "Animal"; this.color = ["white","black","yellow"]; } Animal.prototype.say = function(){ console.log(this.type); } function Cat(){ this.vioce = "喵喵喵"; } Cat.prototype = new Animal(); Cat.prototype.shout = function(){ console.log(this.vioce); } let cat1 = new Cat(); let cat2 = new Cat(); cat1.say(); //"Animal" cat1.say(); //"Animal" cat1.color.push("pink"); console.log(cat1.color); //["white", "black", "yellow", "pink"] console.log(cat2.color); //["white", "black", "yellow", "pink"]
固然,這也好理解不是。假若孫子教會了爺爺某件事,那麼爺爺會把他的本領傳個他的每一個兒子孫子,沒毛病對吧。可是咱們想要的是,孫子本身學會某件事,但不想讓其餘人學會。這樣意思就是每一個實例擁有各自的屬性,不與其餘實例共享。那麼咱們就引入了借用構造函數的概念了。原型鏈
借用構造函數,簡單來講就是在子類構造函數裏面調用父類的構造函數。要怎麼調用?可使用到apply()
和call()
這些方法來實現這個功能。作用域
function Animal(type = "Animal"){ //設置一個參數,若是子類不傳入參數則默認爲"Animal" this.type = type; this.color = ["white","black","yellow"]; } function Cat(type){ Animal.call(this,type); //繼承Animal同時傳入type,也能夠不傳參 } let cat1 = new Cat(); //沒有傳參,type默認爲"Animal" let cat2 = new Cat("Cat"); //傳入"Cat",type則爲"Cat" cat1.color.push("pink"); console.log(cat1.color); //["white", "black", "yellow", "pink"] console.log(cat2.color); //["white", "black", "yellow"] console.log(cat1.type); //"Animal" console.log(cat2.type); //"Cat"
這樣就實現了實例屬性不共享的功能,並且咱們在這個裏面還能夠傳入一個參數,讓其向父類傳參。這是在原型鏈裏面沒法作到的一個功能。至於call()
與apply()
方法,在這暫且不展開,往後另做文章闡明。暫且只須要知道這是改變函數做用域的就行。
那麼,借用構造函數的問題也就是構造函數的問題,方法都定義在構造函數裏面了,複用性就基本涼涼。因此,咱們要組合起來使用。屬性使用借用構造函數模式,而方法則使用原型鏈。
function Animal(){ this.type = "Animal"; this.color = ["white","black","yellow"]; } Animal.prototype.say = function(){ console.log(this.type); } function Cat(){ Animal.call(this); //繼承屬性 this.vioce = "喵喵喵"; } Cat.prototype = new Animal(); //繼承方法 Cat.prototype.shout = function(){ console.log(this.vioce); } let cat1 = new Cat(); let cat2 = new Cat(); cat1.say(); //"Animal" cat1.say(); //"Animal" cat1.color.push("pink"); console.log(cat1.color); //["white", "black", "yellow", "pink"] console.log(cat2.color); //["white", "black", "yellow"]
這一套方法也變成了最經常使用的繼承方法了。可是其中也是有個缺陷,就是每次都會調用兩次父類的構造函數。從而使得實例中與原型對象中創造相同的屬性,不過原型對象中的值卻毫無心義。那有沒有更完美的方法?有,就是寄生組合式繼承。在這裏我就放代碼給你們。
function obj(o){ function F(){} F.prototype = o; return new F(); } function inheritPrototype(sub,super){ let prototype = obj(super.prototype); //至關於拷貝了一個父類對象 prototype.constructor = sub; 加強對象 sub.prototype = prototype; 指定對象 } function Animal(){ this.type = "Animal"; this.color = ["white","black","yellow"]; } Animal.prototype.say = function(){ console.log(this.type); } function Cat(){ Animal.call(this); //繼承屬性 this.vioce = "喵喵喵"; } inheritPrototype(Cat,Animal); Cat.prototype.shout = function(){ console.log(this.vioce); } let cat1 = new Cat(); let cat2 = new Cat(); cat1.say(); //"Animal" cat1.say(); //"Animal" cat1.color.push("pink"); console.log(cat1.color); //["white", "black", "yellow", "pink"] console.log(cat2.color); //["white", "black", "yellow"]
這樣經過一個巧妙的方法就能夠少調用一次父類的構造函數,並且不會賦予原型對象中無心義的屬性。這是被認爲最理想的繼承方法。可是最多人用的仍是上面那個組合式繼承方法。
到這原型鏈的基本概念與用法都已經一一講述,咱們須要注意的地方就是prototype
與__proto__
的關係,重點是分清其中的區別,瞭解父類型跟其子類型的關係,他們之間的聯繫在哪。大概要弄懂的地方,就是要把那兩文章的兩張圖吃透,那麼咱們就已經把原型鏈吃透大半了。
最後假若你們還有什麼不懂的地方,或者博主有什麼遺漏的地方,歡迎你們指出交流。若有興趣能夠持續關注本博主。
原創文章,轉載請註明出處