JavaScript學習總結(五)原型和原型鏈詳解

trigkit4發表於2016-09-16

私有變數和函式

在函式內部定義的變數和函式,如果不對外提供介面,外部是無法訪問到的,也就是該函式的私有的變數和函式。

<script type="text/javascript">
    function Box(){
        var color = "blue";//私有變數
        var fn = function() //私有函式
        {

        }
    }
</script>

這樣在函式物件Box外部無法訪問變數colorfn,他們就變成私有的了:

var obj = new Box();
    alert(obj.color);//彈出 undefined
    alert(obj.fn);//同上

靜態變數和函式

當定義一個函式後通過點號 “.”為其新增的屬性和函式,通過物件本身仍然可以訪問得到,但是其例項卻訪問不到,這樣的變數和函式分別被稱為靜態變數靜態函式

<script type="text/javascript">
    function Obj(){};

    Obj.num = 72;//靜態變數
    Obj.fn = function()  //靜態函式
    {

    }  

    alert(Obj.num);//72
    alert(typeof Obj.fn)//function

    var t = new Obj();
    alert(t.name);//undefined
    alert(typeof t.fn);//undefined
</script>

例項變數和函式

在物件導向程式設計中除了一些庫函式我們還是希望在物件定義的時候同時定義一些屬性和方法,例項化後可以訪問,js也能做到這樣

<script type="text/javascript">
          function Box(){
                this.a=[]; //例項變數
                this.fn=function(){ //例項方法

                }
            }

            console.log(typeof Box.a); //undefined
            console.log(typeof Box.fn); //undefined

            var box=new Box();
            console.log(typeof box.a); //object
            console.log(typeof box.fn); //function
</script>

為例項變數和方法新增新的方法和屬性

<script type="text/javascript">
function Box(){
                this.a=[]; //例項變數
                this.fn=function(){ //例項方法

                }
            }

            var box1=new Box();
            box1.a.push(1);
            box1.fn={};
            console.log(box1.a); //[1]
            console.log(typeof box1.fn); //object

            var box2=new Box();
            console.log(box2.a); //[]
            console.log(typeof box2.fn); //function
</script>

box1中修改了afn,而在box2中沒有改變,由於陣列和函式都是物件,是引用型別,這就說明box1中的屬性和方法與box2中的屬性與方法雖然同名但卻不是一個引用,而是對Box物件定義的屬性和方法的一個複製。

這個對屬性來說沒有什麼問題,但是對於方法來說問題就很大了,因為方法都是在做完全一樣的功能,但是卻又兩份複製,如果一個函式物件有上千和例項方法,那麼它的每個例項都要保持一份上千個方法的複製,這顯然是不科學的,這可腫麼辦呢,prototype應運而生。

基本概念

我們建立的每個函式都有一個prototype屬性,這個屬性是一個指標,指向一個物件,這個物件的用途是包含可以由特定型別的所有例項共享的屬性和方法。那麼,prototype就是通過呼叫建構函式而建立的那個物件例項的原型物件。

使用原型的好處是可以讓物件例項共享它所包含的屬性和方法。也就是說,不必在建構函式中新增定義物件資訊,而是可以直接將這些資訊新增到原型中。使用建構函式的主要問題就是每個方法都要在每個例項中建立一遍。

JavaScript中,一共有兩種型別的值,原始值和物件值。每個物件都有一個內部屬性 prototype ,我們通常稱之為原型。原型的值可以是一個物件,也可以是null。如果它的值是一個物件,則這個物件也一定有自己的原型。這樣就形成了一條線性的鏈,我們稱之為原型鏈

含義

函式可以用來作為建構函式來使用。另外只有函式才有prototype屬性並且可以訪問到,但是物件例項不具有該屬性,只有一個內部的不可訪問的__proto__屬性。__proto__是物件中一個指向相關原型的神祕連結。按照標準,__proto__是不對外公開的,也就是說是個私有屬性,但是Firefox的引擎將他暴露了出來成為了一個共有的屬性,我們可以對外訪問和設定。

<script type="text/javascript">
    var Browser = function(){};
    Browser.prototype.run = function(){
        alert("I'm Gecko,a kernel of firefox");
    }

    var Bro = new Browser();
    Bro.run();
</script>

當我們呼叫Bro.run()方法時,由於Bro中沒有這個方法,所以,他就會去他的__proto__中去找,也就是Browser.prototype,所以最終執行了該run()方法。(在這裡,函式首字母大寫的都代表建構函式,以用來區分普通函式)

當呼叫建構函式建立一個例項的時候,例項內部將包含一個內部指標(__proto__)指向建構函式的prototype,這個連線存在於例項和建構函式的prototype之間,而不是例項與建構函式之間。

<script type="text/javascript">
function Person(name){                             //建構函式
                this.name=name;
            }

            Person.prototype.printName=function() //原型物件
            {
                alert(this.name);
            }

            var person1=new Person('Byron');//例項化物件
            console.log(person1.__proto__);//Person
            console.log(person1.constructor);//自己試試看會是什麼吧
            console.log(Person.prototype);//指向原型物件Person
            var person2=new Person('Frank');
</script>

Person的例項person1中包含了name屬性,同時自動生成一個__proto__屬性,該屬性指向Person的prototype,可以訪問到prototype內定義的printName方法,大概就是這個樣子的:

每個JavaScript函式都有prototype屬性,這個屬性引用了一個物件,這個物件就是原型物件。原型物件初始化的時候是空的,我們可以在裡面自定義任何屬性和方法,這些方法和屬性都將被該建構函式所建立的物件繼承。

那麼,現在問題來了。建構函式、例項和原型物件三者之間有什麼關係呢?

建構函式、例項和原型物件的區別

例項就是通過建構函式建立的。例項一創造出來就具有constructor屬性(指向建構函式)和__proto__屬性(指向原型物件),

建構函式中有一個prototype屬性,這個屬性是一個指標,指向它的原型物件。

原型物件內部也有一個指標(constructor屬性)指向建構函式:Person.prototype.constructor = Person;

例項可以訪問原型物件上定義的屬性和方法。

在這裡person1和person2就是例項,prototype是他們的原型物件。

再舉個例子:

<script type="text/javascript">
    function Animal(name)   //積累建構函式
    {
        this.name = name;//設定物件屬性
    }

    Animal.prototype.behavior = function() //給基類建構函式的prototype新增behavior方法
    {  
        alert("this is a "+this.name);
    }

    var Dog = new Animal("dog");//建立Dog物件
    var Cat = new Animal("cat");//建立Cat物件

    Dog.behavior();//通過Dog物件直接呼叫behavior方法
    Cat.behavior();//output "this is a cat"

    alert(Dog.behavior==Cat.behavior);//output true;
</script>

可以從程式執行結果看出,建構函式的prototype上定義的方法確實可以通過物件直接呼叫到,而且程式碼是共享的。(可以試一下將Animal.prototype.behavior 中的prototype屬性去掉,看看還能不能執行。)在這裡,prototype屬性指向Animal物件。

陣列物件例項

再看個陣列物件的例項。當我們建立出array1這個物件的時候,array1實際在Javascript引擎中的物件模型如下:

var array1 = [1,2,3];

array1物件具有一個length屬性值為3,但是我們可以通過如下的方法來為array1增加元素:

array1.push(4);

push這個方法來自於array1的__proto__成員指向物件的一個方法(Array.prototye.push())。正是因為所有的陣列物件(通過[]來建立的)都包含有一個指向同一個具有push,reverse等方法物件(Array.prototype)的__proto__成員,才使得這些陣列物件可以使用push,reverse等方法。

函式物件例項

function Base() {  
    this.id = "base" 
}

var obj = new Base();

這樣程式碼的結果是什麼,我們在Javascript引擎中看到的物件模型是:

new操作符具體幹了什麼呢?其實很簡單,就幹了三件事情。

var obj  = {};  
obj.__proto__ = Base.prototype;  
Base.call(obj);

原型鏈

原型鏈:當從一個物件那裡調取屬性或方法時,如果該物件自身不存在這樣的屬性或方法,就會去自己關聯的prototype物件那裡尋找,如果prototype沒有,就會去prototype關聯的前輩prototype那裡尋找,如果再沒有則繼續查詢Prototype.Prototype引用的物件,依次類推,直到Prototype.….Prototype為undefined(Object的Prototype就是undefined)從而形成了所謂的“原型鏈”。

<script type="text/javascript">
    function Shape(){
        this.name = "shape";
        this.toString = function(){
            return this.name;
        }
    }
    function TwoShape(){
        this.name = "2 shape";
    }
    function Triangle(side,height){
        this.name = "Triangle";
        this.side = side;
        this.height = height;
        this.getArea = function(){
            return this.side*this.height/2;
        }
    }

    TwoShape.prototype = new Shape();
    Triangle.prototype = new TwoShape();
</script>

這裡,用構造器Shape()新建了一個實體,然後用它去覆蓋該物件的原型。

<script type="text/javascript">
    function Shape(){
        this.name = "shape";
        this.toString = function(){
            return this.name;
        }
    }
    function TwoShape(){
        this.name = "2 shape";
    }
    function Triangle(side,height){
        this.name = "Triangle";
        this.side = side;
        this.height = height;
        this.getArea = function(){
            return this.side*this.height/2;
        }
    }

    TwoShape.prototype = new Shape();
    Triangle.prototype = new TwoShape();

    TwoShape.prototype.constructor = TwoShape;
    Triangle.prototype.constructor = Triangle;

    var my = new Triangle(5,10);
    my.getArea();
    my.toString();//Triangle
    my.constructor;//Triangle(side,height)
</script>

原型繼承

原型繼承:

在原型鏈的末端,就是Object建構函式prototype屬性指向的那個原型物件。這個原型物件是所有物件的祖先,這個老祖宗實現了諸如toString等所有物件天生就該具有的方法。其他內建建構函式,如Function,Boolean,String,DateRegExp等的prototype都是從這個老祖宗傳承下來的,但他們各自又定義了自身的屬性和方法,從而他們的子孫就表現出各自宗族的那些特徵。

ECMAScript中,實現繼承的方法就是依靠原型鏈實現的。

<script type="text/javascript">
    function Father(){             //被繼承的函式叫做超型別(父類,基類)
    this.name = "Jack";
}

function Son(){          //繼承的函式叫做子型別(子類,派生類)
    this.age = 10;
}
//通過原型鏈繼承,賦值給子型別的原型屬性
//new Father()會將father構造裡的資訊和原型裡的資訊都交給Son

Son.prototype = new Father();//Son繼承了Father,通過原型,形成鏈條

var son = new Son();
alert(son.name);//彈出 Jack
</script>

原型鏈的問題:原型鏈雖然很強大,可以用它來實現繼承,但它也存在一些問題。其中最主要的問題來自包含引用型別的值原型。包含引用型別的原型屬性會被所有例項共享;而這也正是為什麼要在建構函式中,而不是在原型物件中定義屬性的原因。在通過原型來實現繼承時,原型實際上回變成另一個型別的例項。於是,原先的例項屬性也就變成了原型的屬性。

在建立子型別的例項時,不能向超型別的建構函式中傳遞引數。實際上,應該說是沒有辦法在不影響所有物件例項的情況下,給超型別的建構函式傳遞引數。再加上剛剛討論的由於原型中包含引用型別值所帶來的問題,實踐中很少會單獨使用原型鏈。

再舉個例子:

<script type="text/javascript">
    function Person(name)
    {
        this.name = name;//設定物件屬性
    };

    Person.prototype.company = "Microsoft";//設定原型的屬性
    Person.prototype.SayHello = function() //原型的方法
    {  
        alert("Hello,I'm "+ this.name+ " of " + this.company);
    };

    var BillGates = new Person("BillGates");//建立person物件
    BillGates.SayHello();//繼承了原型的內容,輸出"Hello,I'm BillGates of Microsoft"

    var Jobs = new Person("Jobs");
    Jobs.company = "Apple";//設定自己的company屬性,掩蓋了原型的company屬性
    Jobs.SayHello = function()
    {
        alert("Hi,"+this.name + " like " + this.company);
    };
    Jobs.SayHello();//自己覆蓋的屬性和方法,輸出"Hi,Jobs like Apple"
    BillGates.SayHello();//Jobs的覆蓋沒有影響原型,BillGates還是照樣輸出
</script>

__ptoto__屬性

__ptoto__屬性(IE瀏覽器不支援)是例項指向原型物件的一個指標,它的作用就是指向建構函式的原型屬性constructor,通過這兩個屬性,就可以訪問原型裡的屬性和方法了。

Javascript中的物件例項本質上是由一系列的屬性組成的,在這些屬性中,有一個內部的不可見的特殊屬性——__proto__,該屬性的值指向該物件例項的原型,一個物件例項只擁有一個唯一的原型。

<script type="text/javascript">
    function Box(){        //大寫,代表建構函式
        Box.prototype.name = "trigkit4";//原型屬性
        Box.prototype.age = "21";
        Box.prototype.run = function()//原型方法
        {  
            return this.name + this.age + 'studying';
        }
    }

    var box1 = new Box();
    var box2 = new Box();
    alert(box1.constructor);//構造屬性,可以獲取建構函式本身,
                            //作用是被原型指標定位,然後得到建構函式本身
</script>

__proto__屬性和prototype屬性的區別

prototypefunction物件中專有的屬性。

__proto__是普通物件的隱式屬性,在new的時候,會指向prototype所指的物件;

__ptoto__實際上是某個實體物件的屬性,而prototype則是屬於建構函式的屬性。__ptoto__只能在學習或除錯的環境下使用。

原型模式的執行流程

1.先查詢建構函式例項裡的屬性或方法,如果有,就立即返回。
2.如果建構函式的例項沒有,就去它的原型物件裡找,如果有,就立即返回

原型物件的

<script type="text/javascript">
    function Box(){        //大寫,代表建構函式
        Box.prototype.name = "trigkit4";//原型屬性
        Box.prototype.age = "21";
        Box.prototype.run = function()//原型方法
        {  
            return this.name + this.age + 'studying';
        }
    }

    var box1 = new Box();
    alert(box1.name);//trigkit4,原型裡的值
    box1.name = "Lee";
    alert(box1.name);//Lee,就進原則

    var box2 = new Box();
    alert(box2.name);//trigkit4,原型的值,沒有被box1修改
</script>

建構函式的

<script type="text/javascript">
    function Box(){                   
        this.name = "Bill";
    }

    Box.prototype.name = "trigkit4";//原型屬性
    Box.prototype.age = "21";
    Box.prototype.run = function()//原型方法
    {  
            return this.name + this.age + 'studying';
    }

    var box1 = new Box();
    alert(box1.name);//Bill,原型裡的值
    box1.name = "Lee";
    alert(box1.name);//Lee,就進原則
</script>

綜上,整理一下:

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

    Person.prototype.name = "trigkit4";
    Person.prototype.say = function(){
        alert("Hi");
    }

    var p1 = new Person();//prototype是p1和p2的原型物件
    var p2 = new Person();//p2為例項化物件,其內部有一個__proto__屬性,指向Person的prototype

    console.log(p1.prototype);//undefined,這個屬性是一個物件,訪問不到
    console.log(Person.prototype);//Person
    console.log(Person.prototype.constructor);//原型物件內部也有一個指標(constructor屬性)指向建構函式
    console.log(p1.__proto__);//這個屬性是一個指標指向prototype原型物件
    p1.say();//例項可以訪問到在原型物件上定義的屬性和方法

</script>
建構函式.prototype = 原型物件
原型物件.constructor = 建構函式(模板)
原型物件.isPrototypeof(例項物件)   判斷例項物件的原型 是不是當前物件

工廠模式

function createObject(name,age){
    var obj = new Object();
    obj.name = name;
    obj.age = age;
    return obj;
}

工廠模式解決了例項化物件大量重複的問題,但還有一個問題,那就是根本無法搞清楚他們到底是哪個物件的例項。

使用建構函式的方法,既解決了重複例項化的問題,又解決了物件識別的問題。

使用建構函式的方法和工廠模式的不同之處在於:

1.建構函式方法沒有顯示的建立物件(new Object());
2.直接將屬性和方法賦值給this物件
3.沒有return 語句

當使用了建構函式,並且new 建構函式(),那麼就在後臺執行了new Object()

函式體內的this代表了new Object()出來的物件

1.判斷屬性是在建構函式的例項裡,還是在原型裡,可以使用`hasOwnProperty()`函式
2.字面量建立的方式使用constructor屬性不會指向例項,而會指向Object,建構函式建立的方式則相反
為什麼指向Object?因為Box.prototype = {};這種寫法其實就是建立了一個新物件。
而每建立一個函式,就會同時建立它的prototype,這個物件也會自動獲取constructor屬性
3.如果是例項方法,不同的例項化,他們的方法地址是不一樣的,是唯一的
4.如果是原型方法,那麼他們的地址的共享的

原型擴充套件:詳解js閉包

JavaScript學習系列文章目錄

相關文章