Javascript基礎系列16:Javascript的原型鏈和繼承詳解

一、繼承的概念

​ 繼承是所有的面向對象的語言最重要的特徵之一。大部分的oop語言的都支持兩種繼承:接口繼承和實現繼承。比如基於類的編程語言Java,對這兩種繼承都支持。從接口繼承抽象方法 (只有方法簽名),從類中繼承實例方法。

​ 但是對JavaScript來說,沒有類和接口的概念(ES6之前),所以只支持實現繼承,而且繼承在 原型鏈 的基礎上實現的。等了解過原型鏈的概念之後,你會發現繼承其實是發生在對象與對象之間。這是與其他編程語言很大的不同。

二、原型鏈的概念

在JavaScript中,將原型鏈作爲實現繼承的主要方法。其基本思想是利用原型讓一個引用類型繼承另一個引用類型的屬性和方法

​ 再回顧下,構造函數、原型(對象)和對象之間的關係。每個構造函數都有一個屬性 prototype 指向一個原型對象,每個原型對象也有一個屬性 constructor 指向函數,通過new 構造函數() 創建出來的對象內部有一個不可見的屬性[[prototype]]指向構造函數的原型。當每次訪問對象的屬性和方法的時候,總是先從p1中找,找不到則再去p1指向的原型中找。

下面我們開始一步步的構造原型鏈,來實現繼承

2.1 更換構造函數的原型

​ 原型其實就是一個對象,只是默認情況下原型對象是瀏覽器會自動幫我們創建的,而且自動讓構造函數的 prototype 屬性指向這個自動創建的原型對象。

​ 其實我們完全可以把原型對象更換成一個我們自定義類型的對象。

看下面的代碼:

<script type="text/javascript">
    //定義一個構造函數。
    function Father () {
        // 添加name屬性.  默認直接賦值了。當然也可以通過構造函數傳遞過來
        this.name = "馬雲";
    }
    //給Father的原型添加giveMoney方法
    Father.prototype.giveMoney = function () {
        alert("我是Father原型中定義的方法");
    }
    //再定義一個構造函數。
    function Son () {
        //添加age屬性
        this.age = 18;
    }
    //關鍵地方:把Son構造方法的原型替換成Father的對象。  因爲原型是對象,任何對象都可以作爲原型
    Son.prototype = new Father();
    //給Son的原型添加getMoney方法
    Son.prototype.getMoney = function () {
        alert("我是Son的原型中定義的方法");
    }
    //創建Son類型的對象
    var son1 = new Son();

    //發現不僅可以訪問Son中定義屬性和Son原型中定義的方法,也可以訪問Father中定義的屬性和Father原型中的方法。
    //這樣就通過原型完成了類型之間的繼承。 
    // Son繼承了Father中的屬性和方法,當然還有Father原型中的屬性和方法。
    son1.giveMoney();
    son1.getMoney();
    alert("Father定義的屬性:" + son1.name);
    alert("Son中定義的屬性:" + son1.age);

</script>

上面的代碼其實就完成了Son繼承Father的過程。那麼到底是怎麼完成的繼承呢?

看下面的示意圖:

說明:

  1. 定義Son構造函數後,我們沒有再使用Son的默認原型,而是把他的默認原型更換成了Father類型對象。
  2. 這時,如果這樣訪問 son1.name, 則先在son1中查找name屬性,沒有然後去他的原型( Father對象)中找到了,所以是”馬雲”。
  3. 如果這樣訪問 son1.giveMoney(), 先在son1中找這個方法,找不到去他的原型中找,仍然找不到,則再去這個原型的原型中去找,然後在 Father的原型對象中 找到了。
  4. 從圖中可以看出來,在訪問屬性和方法的時候,查找的順序是這樣的:對象->原型->原型的原型->…->原型鏈的頂端。 就像一個鏈條一樣,這樣 由原型連成的”鏈條”,就是我們經常所說的原型鏈。
  5. 從上面的分析可以看出,通過原型鏈的形式就完成了JavaScript的繼承。

2.2 默認頂端原型

​ 其實上面原型鏈還缺少一環。

​ 在 JavaScript 中所有的類型如果沒有指明繼承某個類型,則默認是繼承的 Object 類型。這種 默認繼承也是通過原型鏈的方式完成的。

下面的圖就是一個完整的原型鏈:

mark

說明:

  1. 原型鏈的頂端一定是Object這個構造函數的原型對象。這也是爲什麼我們隨意創建一個對象,就有很多方法可以調用,其實這些方法都是來自Object的原型對象。
  2. 通過對象訪問屬性、方法的時候,一定是會通過原型鏈來查找的,直到原型鏈的頂端。
  3. 一旦有了繼承,就會出現多態的情況。假設需要一個Father類型的數據,那麼你給一個Father對象,或Son對象都是沒有任何問題的。而在實際執行的過程中,一個方法的具體執行結果,就看在原型鏈中的查找過程了。給一個實際的Father對象則從Fahter的原型鏈中查找,給一個實際的Son則從Son的原型鏈中查找。
  4. 因爲繼承的存在,Son的對象,也可以看出Father類型的對象和Object類型的對象。 子類型對象可以看出一個特殊的父類型對象。

2.3 測試數據的類型

​ 到目前爲止,我們有3中方法來測試數據的類型。

  1. typeof:一般用來測試簡單數據類型和函數的類型。如果用來測試對象,則會一直返回object,沒有太大意義。
<script type="text/javascript">
    alert(typeof 5); // number
    var v = "abc";
    alert(typeof v);  // string
    alert(typeof function () {

    });  //funcion
    function Person () {

    }
    alert(typeof new Person()); //object

</script>
  1. instanceof: 用來測試一個對象是不是屬於某個類型。結果爲boolean值。
<script type="text/javascript">
    function Father () {
    }
    function Son () {   
    }

    Son.prototype = new Father();
    var son = new Son();
    alert(son instanceof Son);  // true
    // Son通過原型繼承了Father
    alert(son instanceof Father);  // true
    //Father又默認繼承了Objcet
    alert(son instanceof Object); // true
</script>
  1. isPrototypeOf( 對象 ) : 這是個 原型對象 的方法,參數傳入一個對象,判斷參數對象是不是由這個原型派生出來的。 也就是判斷這個原型是不是參數對象原型鏈中的一環。
<script type="text/javascript">
    function Father () {

    }
    function Son () {

    }

    Son.prototype = new Father();
    var son = new Son();
    alert(Son.prototype.isPrototypeOf(son));  // true
    alert(Father.prototype.isPrototypeOf(son)); // true
    alert(Object.prototype.isPrototypeOf(son)); // true
</script>

2.4 原型鏈在繼承中的缺陷

​ 原型鏈並非完美無缺,也是存在一些問題的。

2.4.1 父類型的屬性共享問題

​ 在原型鏈中,父類型的構造函數創建的對象,會成爲子類型的原型。那麼父類型中定義的實例屬性,就會成爲子類型的原型屬性。對子類型來說,這和我們以前說的在原型中定義方法,構造函數中定義屬性是違背的。子類型原型(父類型對象)中的屬性被所有的子類型的實例所共有,如果有個一個實例去更改,則會很快反應的其他的實例上。

看下面的代碼:

<script type="text/javascript">
    function Father () {
        this.girls = ["志玲", "鳳姐"];
    }
    function Son () {

    }
    // 子類的原型對象中就有一個屬性 girls ,是個數組
    Son.prototype = new Father();   
    var son1 = new Son();
    var son2 = new Son();
    //給son1的girls屬性的數組添加一個元素
    son1.girls.push("亦非");
    //這時,發現son2中的girls屬性的數組內容也發生了改變
    alert(son2.girls);  // "志玲", "鳳姐", "亦非"
</script>

2.4.2 向父類型的構造函數中傳遞參數問題

​ 在原型鏈的繼承過程中,只有一個地方用到了父類型的構造函數,Son.prototype = new Father();。只能在這個一個位置傳遞參數,但是這個時候傳遞的參數,將來對子類型的所有的實例都有效。

​ 如果想在創建子類型對象的時候傳遞參數是沒有辦法做到的。

​ 如果想創建子類對象的時候,傳遞參數,只能另闢他法。

三、借用構造函數調用”繼承”

3.1 借用的方式

借用構造函數調用 繼承,又叫僞裝調用繼承或冒充調用繼承。雖然有了繼承兩個字,但是這種方法從本質上並沒實現繼承,只是完成了構造方法的調用而已。

​ 使用 callapply 這兩個方法完成函數借調。這兩個方法的功能是一樣的,只有少許的區別(暫且不管)。功能都是更改一個構造方法內部的 this 指向到指定的對象上。

看下面的代碼:

<script type="text/javascript">
    function Father (name,age) {
        this.name = name;
        this.age = age;
    }
    //如果這樣直接調用,那麼father中的this只的是 window。 因爲其實這樣調用的: window.father("李四", 20)
    // name 和age 屬性就添加到了window屬性上
    Father("李四", 20);
    alert("name:" + window.name + "\nage:" + window.age);  //可以正確的輸出

    //使用call方法調用,則可以改變this的指向
    function Son (name, age, sex) {
        this.sex = sex;
        //調用Father方法(看成普通方法),第一個參數傳入一個對象this,則this(Son類型的對象)就成爲了Father中的this
        Father.call(this, name, age);
    }
    var son = new Son("張三", 30, "男");
    alert("name:" + son.name + "\nage:" + son.age + "\nsex:" + son.sex);
    alert(son instanceof Father); //false
</script>

函數借調的方式還有別的實現方式,但是原理都是一樣的。但是有一點要記住,這裏其實並沒有真的繼承,僅僅是調用了Father構造函數而已。也就是說,son對象和Father沒有任何的關係。

3.2 借用的缺陷

Father的原型對象中的共享屬性和方法,Son沒有辦法獲取。因爲這個根本就不是真正的繼承。

四、組合繼承

​ 組合函數利用了原型繼承和構造函數借調繼承的優點,組合在一起。成爲了使用最廣泛的一種繼承方式。

<script type="text/javascript">
    //定義父類型的構造函數
    function Father (name,age) {
        // 屬性放在構造函數內部
        this.name = name;
        this.age = age;
        // 方法定義在原型中
        if((typeof Father.prototype.eat) != "function"){
            Father.prototype.eat = function () {
                alert(this.name + " 在吃東西");
            }
        }  
    }
    // 定義子類類型的構造函數
    function Son(name, age, sex){
        //借調父類型的構造函數,相當於把父類型中的屬性添加到了未來的子類型的對象中
        Father.call(this, name, age);
        this.sex = sex;
    }
    //修改子類型的原型爲父類型的對象。這樣就可以繼承父類型中的方法了。
    Son.prototype = new Father( );
    var son1 = new Son("志玲", 30, "女");
    alert(son1.name);
    alert(son1.sex);
    alert(son1.age);
    son1.eat();
</script>

說明:

  1. 組合繼承是我們實際使用中最常用的一種繼承方式。
  2. 可能有個地方有些人會有疑問:Son.prototype = new Father( );這不照樣把父類型的屬性給放在子類型的原型中了嗎,還是會有共享問題呀。但是不要忘記了,我們在子類型的構造函數中借調了父類型的構造函數,也就是說,子類型的原型(也就是Father的對象)中有的屬性,都會被子類對象中的屬性給覆蓋掉。就是這樣的。
發佈了79 篇原創文章 · 獲贊 375 · 訪問量 29萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章