JavaScript由淺及深敲開原型鏈(二)

1、對象的繼承

1.瞭解原型鏈

在上一篇咱們講過關於原型對象的概念,固然若是不瞭解的建議去翻看第一篇文章,文末附有鏈接。咱們知道每一個對象都有各自的原型對象,那麼當咱們把一個對象的實例當作另一個對象的原型對象。。這樣這個對象就擁有了另一個引用類型的全部方法與屬性,當咱們再把該對象的實例賦予另外一個原型對象時,這樣又把這些方法繼承下去。如此層層遞進,對象與原型間存在連接關係,這樣就構成了原型鏈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);
    }
}

2.原型鏈的問題

接下來咱們談談原型鏈的問題。提及原型鏈的問題咱們大概能夠聯想到原型對象的問題:其屬性與方法會被全部實例共享,那麼在原型鏈中亦是如此。繼承

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"]

固然,這也好理解不是。假若孫子教會了爺爺某件事,那麼爺爺會把他的本領傳個他的每一個兒子孫子,沒毛病對吧。可是咱們想要的是,孫子本身學會某件事,但不想讓其餘人學會。這樣意思就是每一個實例擁有各自的屬性,不與其餘實例共享。那麼咱們就引入了借用構造函數的概念了。原型鏈

3.借用構造函數

借用構造函數,簡單來講就是在子類構造函數裏面調用父類的構造函數。要怎麼調用?可使用到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()方法,在這暫且不展開,往後另做文章闡明。暫且只須要知道這是改變函數做用域的就行。

那麼,借用構造函數的問題也就是構造函數的問題,方法都定義在構造函數裏面了,複用性就基本涼涼。因此,咱們要組合起來使用。屬性使用借用構造函數模式而方法則使用原型鏈

4.組合繼承

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__的關係,重點是分清其中的區別,瞭解父類型跟其子類型的關係,他們之間的聯繫在哪。大概要弄懂的地方,就是要把那兩文章的兩張圖吃透,那麼咱們就已經把原型鏈吃透大半了。

最後假若你們還有什麼不懂的地方,或者博主有什麼遺漏的地方,歡迎你們指出交流。若有興趣能夠持續關注本博主。

原創文章,轉載請註明出處

相關文章
相關標籤/搜索