1. 程式人生 > >JavaScript中原型鏈的那些事

JavaScript中原型鏈的那些事

單鏈表 分享圖片 mage classes 替換 afa 就是 區別 urn

引言

在面向對象的語言中繼承是非常重要的概念,許多面向對象語言都支持兩種繼承方式:接口繼承和實現繼承。接口繼承制只繼承方法簽名,而實現繼承繼承實際的方法。在ECMAScript中函數沒有簽名,所以ECMAScript無法實現接口繼承,只能實現實現繼承。那麽是怎麽實現實現繼承的呢??這就要說一說JS中的原型鏈了。

原型鏈的定義

什麽是原型鏈?這個問題很簡單,其基本思想就是利用原型讓一個引用類型繼承另一個引用類型的屬性和方法。

我們先來回顧一下構造函數,原型,實例之間的關系。每一個構造函數都有一個原型對象,原型對象中包含一個指向構造函數的指針,而實例都包含一個指向原型對象的內部指針。在原型對象中通過prototype指向構造函數,而在實例中通過__proto__指向原型對象,但是該屬性是區分瀏覽器的,這是部分瀏覽器為實例對象添加的屬性,在ECMAScript中表現為[[prototype]]。

現在我們已經知道了原型對象中存在一個指針指向構造函數,現在我們讓原型對象等於另一個類型的實例,此時的原型對象將包含一個指向另一個原型的指針,那麽另一個原型中也包含一個指向另一個構造函數的指針。加入另一個原型有事另一個類型的實例,那麽如此層層遞進,就構成了實例與原型的鏈條。這就是原型鏈的基本概念。

我的理解:在我看來,我們可以將原型鏈理解為一個單鏈表,每一個原型對象都包含一個指向另一個原型對象的指針,如此遞進的鏈接起來,形式單鏈表(但並不是說原型鏈的結構就是單鏈表,這樣只是便於理解)。

function SuperType() {
    this.property = true;
}

Super.prototype.getSuperValue 
= function() { return this.property; } function SubType() { this.subproperty = false; } SubType.prototype = new SuperType(); SubType.prototype.getSubValue = function() { return this.subproperty; } var instance = new SubType(); console.log(instance.getSuperValue()); //輸出:true
技術分享圖片

可以看出來,在上述代碼中,原型鏈的繼承是通過創建SuperType的實例並將實例賦給SubType的原型實現的。本質就是重寫原型對象,換成一個新類型的實例。原來存在於SuperType的實例中的所有屬性和方法都會存在於SubType.prototype中。最終結果是這樣的:instance指向SubType的原型,SubType的原型又指向SuperType的原型。

有一點需要註意的是,instance.constructor現在指向的不是SubType,而是SuperType,原因是SubType的原型指向了另外一個對象SuperType的原型,而這個原型對象的constructor屬性指向的是SuperType。

原型搜索機制

通過原型鏈的實現擴展了原型的原型搜索機制。原型搜索機制就是以讀寫模式訪問一個實例屬性時,首先會在實例中搜索該屬性,如果沒有就會繼續搜索實例的原型。通過原型鏈繼承的情況下,搜索過程就會沿著原型鏈繼續向上。首先會搜索實例,在實例中查找是否有需要訪問的屬性,如果沒有,將會搜索實例的原型,看原型中是否有定義的原型屬性,如果沒有將會通過原型鏈向上找指向的原型,在找不到屬性或方法的情況下回一環一環的到原型鏈的末端才會停止。

prototype和__proto__的區別

在學習原型鏈的時候經常搞不懂prototype和__proto__的區別,所以把這兩個東西的比較摘出來寫成一塊。

技術分享圖片

技術分享圖片

__proto__屬性的來歷:創建了自定義的構造函數後其原型對象只會取得constructor屬性,其他的方法都是從Object繼承的,當使用構造函數的創建一個新的實例的時候該實例內部包含一個指針指向構造函數的原型對象。在ECMAScript中管這個指針叫做[[Prototype]]。在腳本中沒有標準的方式訪問這個指針。但Firefox、Safari、Chrome在每個對象上都支持一個屬性__proto__;但是在其他的實現中,這個屬性對腳本是完全不可見的。

默認的原型

所有引用類型默認繼承Object,而這個繼承也是通過原型鏈實現的。所有函數的默認原型都是Object的實例,所以在默認原型都會包含一個內部指針,指向Object.prototype。這也是自定義類型都會竭誠toString()、valueOf()等默認方法的原因

謹慎定義方法

1. 給原型添加方法的代碼一定要放在替換原型的語句之後。

如下例:

        function SuperType() {
            this.property = true;
        }

        SuperType.prototype.getSuperValue = function() {
            return this.property;
        }

        function SubType() {
            this.subpeoperty = false;
        }

        SubType.prototype = new SuperType();

        //添加新方法
        SubType.prototype.getSubValue = function() {
            return this.subpeoperty;
        }
        
        //重寫超類型中的方法
        SubType.prototype.getSuperValue = function() {
            return false;
        }

        var instance = new SubType();
        console.log(instance.getSuperValue());

        //輸出:false
技術分享圖片

在上面代碼中,重寫的方法會屏蔽原來的方法。當通過SubType的實例調用getSuperValue()時,調用的就是重新定義的方法,但通過SuperType的實例調用getSuperValue()時,還會調用原來的方法。

2. 在通過原型鏈實現繼承的時候,不能使用對象字面量創建原型方法。

這樣會重寫原型鏈。如下例所示:

            function SuperType() {
                this.property = true;
            }

            SuperType.prototype.getSuperValue = function() {
                return this.property;
            }

            function SubType() {
                this.subproperty = false;
            }

            SubType.prototype = new SuperType();

            SubType.prototype = {
                getSubValue:function() {
                    return this.subproperty;
                },

                someOtherMethod: function() {
                    return false;
                }
            }

            var instace = new SubType();
            console.log(instace.getSuperValue());
技術分享圖片

輸出:

技術分享圖片技術分享圖片?

在上面的例子中,我們把SuperType的實例賦值給原型,緊接著有獎原型替換成一個對象字面量,由於現在的原型包含的是一個Object實例,而非SuperType的實例,一次原型鏈已經被切斷,SuperType和SubType已經沒有關系了。

原型鏈的問題

1. 我們都只知道引用類型的對象中存儲的是指向堆內存的指針,所以包含引用類型值的原型屬性會被所有實例共享。因為在原型對象中的引用類型只是一個指針,在實例化對象的時候,指針復制,但是指針指向沒有發生變化。這也是為什麽要在構造函數中,而不是在原型對象中定義屬性的原因了。看下面的代碼:

            function SuperType() {
                this.colors = [‘red‘, ‘blue‘, ‘green‘];
            }

            function SubType() {
                
            }

            SubType.prototype = new SuperType();

            var instace1 = new SubType();
            instace1.colors.push(‘black‘);
            console.log(instace1.colors);

            var instace2 = new SubType();
            console.log(instace2.colors);


            //輸出:
            // ["red", "blue", "green", "black"]
            // ["red", "blue", "green", "black"]

技術分享圖片

需要註意的是,在JS中基本類型值的原型屬性並不是這樣的:

            function SuperType() {
                this.property = true;
            }

            function SubType() {
                
            }

            SubType.prototype = new SuperType();

            var instace1 = new SubType();
            instace1.property = false;
            console.log(instace1.property);

            var instace2 = new SubType();
            console.log(instace2.property);


            //輸出:
            // false
            // true
技術分享圖片

原因相比通過上面的實例大家都知道了,在JS中基本類型值的存儲並不是通過指針。

2.在創建子類型的實例時,不能向超類型的構造函數中傳遞參數。

基於這些問題,在實踐中我們會很少單獨使用原型鏈,至於怎麽在實踐中更好地使用原型鏈,下一篇博客我會詳細講解。

以上~~

JavaScript中原型鏈的那些事