1. 程式人生 > >JavaScript原型徹底理解 JavaScript原型徹底理解2---繼承中的原型鏈

JavaScript原型徹底理解 JavaScript原型徹底理解2---繼承中的原型鏈

https://blog.csdn.net/u012468376/article/details/53121081 

https://blog.csdn.net/u012468376/article/details/53127929


一、什麼是原型

原型是Javascript中的繼承的基礎,JavaScript的繼承就是基於原型的繼承。

1.1 函式的原型物件

​ 在JavaScript中,我們建立一個函式A(就是宣告一個函式), 那麼瀏覽器就會在記憶體中建立一個物件B,而且每個函式都預設會有一個屬性 prototype 指向了這個物件( 即:prototype的屬性的值是這個物件

 )。這個物件B就是函式A的原型物件,簡稱函式的原型。這個原型物件B 預設會有一個屬性 constructor 指向了這個函式A ( 意思就是說:constructor屬性的值是函式A )。

​ 看下面的程式碼:

<body>
    <script type="text/javascript">
        /*
            宣告一個函式,則這個函式預設會有一個屬性叫 prototype 。而且瀏覽器會自動按照一定的規則
            建立一個物件,這個物件就是這個函式的原型物件,prototype屬性指向這個原型物件。這個原型物件
            有一個屬性叫constructor 執行了這個函式

            注意:原型物件預設只有屬性:constructor。其他都是從Object繼承而來,暫且不用考慮。
        */
function Person () { }
</script> </body>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

下面的圖描述了宣告一個函式之後發生的事情:

1.2 使用建構函式建立物件

​ 當把一個函式作為建構函式 (理論上任何函式都可以作為建構函式) 使用new建立物件的時候,那麼這個物件就會存在一個預設的不可見的屬性,來指向了建構函式的原型物件。 這個不可見的屬性我們一般用 [[prototype]] 來表示,只是這個屬性沒有辦法直接訪問到。

​ 看下面的程式碼:

<body>
    <script type="text/javascript">
        function Person () {

        }   
        /*
            利用建構函式建立一個物件,則這個物件會自動新增一個不可見的屬性 [[prototype]], 而且這個屬性
            指向了建構函式的原型物件。
        */
        var p1 = new Person();
    </script>
</body>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

觀察下面的示意圖:

說明:

  1. 從上面的圖示中可以看到,建立p1物件雖然使用的是Person建構函式,但是物件創建出來之後,這個p1物件其實已經與Person建構函式沒有任何關係了,p1物件的[[ prototype ]]屬性指向的是Person建構函式的原型物件。
  2. 如果使用new Person()建立多個物件,則多個物件都會同時指向Person建構函式的原型物件。
  3. 我們可以手動給這個原型物件新增屬性和方法,那麼p1,p2,p3…這些物件就會共享這些在原型中新增的屬性和方法。
  4. 如果我們訪問p1中的一個屬性name,如果在p1物件中找到,則直接返回。如果p1物件中沒有找到,則直接去p1物件的[[prototype]]屬性指向的原型物件中查詢,如果查詢到則返回。(如果原型中也沒有找到,則繼續向上找原型的原型—原型鏈。 後面再講)。
  5. 如果通過p1物件添加了一個屬性name,則p1物件來說就遮蔽了原型中的屬性name。 換句話說:在p1中就沒有辦法訪問到原型的屬性name了。
  6. 通過p1物件只能讀取原型中的屬性name的值,而不能修改原型中的屬性name的值。 p1.name = “李四”; 並不是修改了原型中的值,而是在p1物件中給添加了一個屬性name。

看下面的程式碼:

<body>
    <script type="text/javascript">
        function Person () {        
        }
        // 可以使用Person.prototype 直接訪問到原型物件
        //給Person函式的原型物件中新增一個屬性 name並且值是 "張三"
        Person.prototype.name = "張三";
        Person.prototype.age = 20;

        var p1 = new Person();
        /*
            訪問p1物件的屬性name,雖然在p1物件中我們並沒有明確的新增屬性name,但是
            p1的 [[prototype]] 屬性指向的原型中有name屬性,所以這個地方可以訪問到屬性name
            就值。
            注意:這個時候不能通過p1物件刪除name屬性,因為只能刪除在p1中刪除的物件。
        */
        alert(p1.name);  // 張三

        var p2 = new Person();
        alert(p2.name);  // 張三  都是從原型中找到的,所以一樣。

        alert(p1.name === p2.name);  // true

        // 由於不能修改原型中的值,則這種方法就直接在p1中添加了一個新的屬性name,然後在p1中無法再訪問到
        //原型中的屬性。
        p1.name = "李四";
        alert("p1:" + p1.name);
        // 由於p2中沒有name屬性,則對p2來說仍然是訪問的原型中的屬性。    
        alert("p2:" + p2.name);  // 張三  
    </script>
</body>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31

二、與原型有關的幾個屬性和方法

2.1 prototype屬性

​ prototype 存在於建構函式中 (其實任意函式中都有,只是不是建構函式的時候prototype我們不關注而已) ,他指向了這個建構函式的原型物件。

​ 參考前面的示意圖。

2.2 constructor屬性

​ constructor屬性存在於原型物件中,他指向了建構函式

看下面的程式碼:

<script type="text/javascript">
    function Person () {
    }
    alert(Person.prototype.constructor === Person); // true
    var p1 = new Person();
    //使用instanceof 操作符可以判斷一個物件的型別。  
    //typeof一般用來獲取簡單型別和函式。而引用型別一般使用instanceof,因為引用型別用typeof 總是返回object。
    alert(p1 instanceof Person);    // true
</script>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

我們根據需要,可以Person.prototype 屬性指定新的物件,來作為Person的原型物件。

但是這個時候有個問題,新的物件的constructor屬性則不再指向Person構造函數了。

看下面的程式碼:

<script type="text/javascript">
    function Person () {

    }
    //直接給Person的原型指定物件字面量。則這個物件的constructor屬性不再指向Person函式
    Person.prototype = {
        name:"志玲",
        age:20
    };
    var p1 = new Person();
    alert(p1.name);  // 志玲

    alert(p1 instanceof Person); // true
    alert(Person.prototype.constructor === Person); //false
    //如果constructor對你很重要,你應該在Person.prototype中新增一行這樣的程式碼:
    /*
    Person.prototype = {
        constructor : Person    //讓constructor重新指向Person函式
    }
    */
</script>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

2.3 __proto__ 屬性(注意:左右各是2個下劃線)

​ 用構造方法建立一個新的物件之後,這個物件中預設會有一個不可訪問的屬性 [[prototype]] , 這個屬性就指向了構造方法的原型物件。

​ 但是在個別瀏覽器中,也提供了對這個屬性[[prototype]]的訪問(chrome瀏覽器和火狐瀏覽器。ie瀏覽器不支援)。訪問方式:p1.__proto__

​ 但是開發者儘量不要用這種方式去訪問,因為操作不慎會改變這個物件的繼承原型鏈。

<script type="text/javascript">
    function Person () {

    }
    //直接給Person的原型指定物件字面量。則這個物件的constructor屬性不再指向Person函式
    Person.prototype = {
        constructor : Person,
        name:"志玲",
        age:20
    };
    var p1 = new Person();

    alert(p1.__proto__ === Person.prototype);   //true

</script>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

2.4 hasOwnProperty() 方法

​ 大家知道,我們用去訪問一個物件的屬性的時候,這個屬性既有可能來自物件本身,也有可能來自這個物件的[[prototype]]屬性指向的原型。

​ 那麼如何判斷這個物件的來源呢?

​ hasOwnProperty方法,可以判斷一個屬性是否來自物件本身。

<script type="text/javascript">
    function Person () {

    }
    Person.prototype.name = "志玲";
    var p1 = new Person();
    p1.sex = "女";
    //sex屬性是直接在p1屬性中新增,所以是true
    alert("sex屬性是物件本身的:" + p1.hasOwnProperty("sex"));
    // name屬性是在原型中新增的,所以是false
    alert("name屬性是物件本身的:" + p1.hasOwnProperty("name"));
    //  age 屬性不存在,所以也是false
    alert("age屬性是存在於物件本身:" + p1.hasOwnProperty("age"));

</script>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

所以,通過hasOwnProperty這個方法可以判斷一個物件是否在物件本身新增的,但是不能判斷是否存在於原型中,因為有可能這個屬性不存在。

也即是說,在原型中的屬性和不存在的屬性都會返回fasle。

如何判斷一個屬性是否存在於原型中呢?

2.5 in 操作符

​ in操作符用來判斷一個屬性是否存在於這個物件中。但是在查詢這個屬性時候,現在物件本身中找,如果物件找不到再去原型中找。換句話說,只要物件和原型中有一個地方存在這個屬性,就返回true

<script type="text/javascript">
    function Person () {

    }
    Person.prototype.name = "志玲";
    var p1 = new Person();
    p1.sex = "女";
    alert("sex" in p1);     // 物件本身新增的,所以true
    alert("name" in p1);    //原型中存在,所以true
    alert("age" in p1);     //物件和原型中都不存在,所以false

</script>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

回到前面的問題,如果判斷一個屬性是否存在於原型中:

如果一個屬性存在,但是沒有在物件本身中,則一定存在於原型中。

<script type="text/javascript">
    function Person () {
    }
    Person.prototype.name = "志玲";
    var p1 = new Person();
    p1.sex = "女";

    //定義一個函式去判斷原型所在的位置
    function propertyLocation(obj, prop){
        if(!(prop in obj)){
            alert(prop + "屬性不存在");
        }else if(obj.hasOwnProperty(prop)){
            alert(prop + "屬性存在於物件中");
        }else {
            alert(prop + "物件存在於原型中");
        }
    }
    propertyLocation(p1, "age");
    propertyLocation(p1, "name");
    propertyLocation(p1, "sex");
</script
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

三、組合原型模型和建構函式模型建立物件

3.1 原型模型建立物件的缺陷

​ 原型中的所有的屬性都是共享的。也就是說,用同一個建構函式建立的物件去訪問原型中的屬性的時候,大家都是訪問的同一個物件,如果一個物件對原型的屬性進行了修改,則會反映到所有的物件上面。

​ 但是在實際使用中,每個物件的屬性一般是不同的。張三的姓名是張三,李四的姓名是李四。

​ ==但是,這個共享特性對 方法(屬性值是函式的屬性)又是非常合適的。==所有的物件共享方法是最佳狀態。這種特性在c#和Java中是天生存在的。

3.2 建構函式模型建立物件的缺陷

​ 在建構函式中新增的屬性和方法,每個物件都有自己獨有的一份,大家不會共享。這個特性對屬性比較合適,但是對方法又不太合適。因為對所有物件來說,他們的方法應該是一份就夠了,沒有必要每人一份,造成記憶體的浪費和效能的低下。

<script type="text/javascript">
    function Person() {
        this.name = "李四";
        this.age = 20;
        this.eat = function() {
            alert("吃完東西");
        }
    }
    var p1 = new Person();
    var p2 = new Person();
    //每個物件都會有不同的方法
    alert(p1.eat === p2.eat); //fasle
</script>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

可以使用下面的方法解決:

<script type="text/javascript">
    function Person() {
        this.name = "李四";
        this.age = 20;
        this.eat = eat;
    }
    function eat() {
        alert("吃完東西");
    }
    var p1 = new Person();
    var p2 = new Person();
    //因為eat屬性都是賦值的同一個函式,所以是true
    alert(p1.eat === p2.eat); //true
</script>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

但是上面的這種解決方法具有致命的缺陷:封裝性太差。使用面向物件,目的之一就是封裝程式碼,這個時候為了效能又要把程式碼抽出物件之外,這是反人類的設計。

3.3 使用組合模式解決上述兩種缺陷

​ 原型模式適合封裝方法,建構函式模式適合封裝屬性,綜合兩種模式的優點就有了組合模式。

<script type="text/javascript">
    //在構造方法內部封裝屬性
    function Person(name, age) {
        this.name = name;
        this.age = age;
    }
    //在原型物件內封裝方法
    Person.prototype.eat = function (food) {
        alert(this.name + "愛吃" + food);
    }
    Person.prototype.play = function (playName) {
        alert(this.name + "愛玩" + playName);
    }

    var p1 = new Person("李四", 20);
    var p2 = new Person("張三", 30);
    p1.eat("蘋果");
    p2.eat("香蕉");
    p1.play("志玲");
    p2.play("鳳姐");
</script>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

四、動態原型模式建立物件

​ 前面講到的組合模式,也並非完美無缺,有一點也是感覺不是很完美。把構造方法和原型分開寫,總讓人感覺不舒服,應該想辦法把構造方法和原型封裝在一起,所以就有了動態原型模式。

​ 動態原型模式把所有的屬性和方法都封裝在構造方法中,而僅僅在需要的時候才去在構造方法中初始化原型,又保持了同時使用建構函式和原型的優點。

看下面的程式碼:

<script type="text/javascript">
    //構造方法內部封裝屬性
    function Person(name, age) {
        //每個物件都新增自己的屬性
        this.name = name;
        this.age = age;
        /*
            判斷this.eat這個屬性是不是function,如果不是function則證明是第一次建立物件,
            則把這個funcion新增到原型中。
            如果是function,則代表原型中已經有了這個方法,則不需要再新增。
            perfect!完美解決了效能和程式碼的封裝問題。
        */
        if(typeof this.eat !== "function"){
            Person.prototype.eat = function () {
                alert(this.name + " 在吃");
            }
        }
    }
    var p1 = new Person("志玲", 40);
    p1.eat();   
</script>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

說明:

  • 組合模式和動態原型模式是JavaScript中使用比較多的兩種建立物件的方式。
  • 建議以後使用動態原型模式。他解決了組合模式的封裝不徹底的缺點。



JavaScript原型徹底理解2---繼承中的原型鏈

一、繼承的概念

​ 繼承是所有的面向物件的語言最重要的特徵之一。大部分的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>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33

上面的程式碼其實就完成了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型別的資料,那麼你給一個Fa