1. 程式人生 > >JavaScript prototype原型和原型鏈詳解

JavaScript prototype原型和原型鏈詳解

用過JavaScript的同學們肯定都對prototype如雷貫耳,但是這究竟是個什麼東西卻讓初學者莫衷一是,只知道函式都會有一個prototype屬性,可以為其新增函式供例項訪問,其它的就不清楚了,最近看了一些 JavaScript高階程式設計,終於揭開了其神祕面紗。

每個函式都有一個prototype屬性,這個屬性是指向一個物件的引用,這個物件稱為原型物件,原型物件包含函式例項共享的方法和屬性,也就是說將函式用作建構函式呼叫(使用new操作符呼叫)的時候,新建立的物件會從原型物件上繼承屬性和方法。不像傳統的面嚮物件語言,Javascript的繼承機制基於原型,而不是Class類。

1、私有變數、函式

在具體說prototype前說幾個相關的東東,可以更好的理解prototype的設計意圖。在瞭解JavaScript原型鏈之前,有必要先了解一下JavaScript的作用域鏈。JavaScript的函式作用域,在函式內定義的變數和函式如果不對外提供介面,那麼外部將無法訪問到,也就是變為私有變數和私有函式。

function Obj(){
    var a=0; //私有變數
    var fn=function(){ //私有函式

   }
}

這樣在函式物件Obj外部無法訪問變數a和函式fn,它們就變成私有的,只能在Obj內部使用,即使是函式Obj的例項仍然無法訪問這些變數和函式

var
o=new Obj(); console.log(o.a); //undefined console.log(o.fn); //undefined

2、靜態變數、函式

當定義一個函式後通過 “.”為其新增的屬性和函式,通過物件本身仍然可以訪問得到,但是其例項卻訪問不到,這樣的變數和函式分別被稱為靜態變數和靜態函式,用過Java、C#的同學很好理解靜態的含義。

function Obj(){}
    Obj.a=0; //靜態變數
    Obj.fn=function(){ //靜態函式       
}

console.log(Obj.a); //0
console.log(typeof Obj.fn); //function
var o=new Obj(); console.log(o.a); //undefined console.log(typeof o.fn); //undefined

3、例項變數、函式

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

function Obj(){
    this.a=[]; //例項變數
    this.fn=function(){ //例項方法    
    }
}

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

var o=new Obj();
console.log(typeof o.a); //object
console.log(typeof o.fn); //function

這樣可以達到上述目的,然而

function Obj(){
    this.a=[]; //例項變數
    this.fn=function(){ //例項方法

    }
}

var o1=new Obj();
o1.a.push(1);
o1.fn={};
console.log(o1.a); //[1]
console.log(typeof o1.fn); //object
var o2=new Obj();
console.log(o2.a); //[]
console.log(typeof o2.fn); //function

上面的程式碼執行結果完全符合預期,但同時也說明一個問題,在o1中修改了a和fn,而在o2中沒有改變,由於陣列和函式都是物件,是引用型別,這就說明o1中的屬性和方法與o2中的屬性與方法雖然同名但卻不是一個引用,而是對Obj物件定義的屬性和方法的一個複製。

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

4 . 普通物件與函式物件

JavaScript 中,萬物皆物件!但物件也是有區別的。分為普通物件和函式物件,Object ,Function 是JS自帶的函式物件。下面舉例說明

 function f1(){};
 var f2 = function(){};
 var f3 = new Function('str','console.log(str)');

 var o3 = new f1();
 var o1 = {};
 var o2 =new Object();

 console.log(typeof Object); //function
 console.log(typeof Function); //function
 console.log(typeof o1); //object
 console.log(typeof o2); //object
 console.log(typeof o3); //object
 console.log(typeof f1); //function
 console.log(typeof f2); //function
 console.log(typeof f3); //function 

在上面的例子中 o1 o2 o3 為普通物件,f1 f2 f3 為函式物件。怎麼區分,其實很簡單,凡是通過 new Function() 建立的物件都是函式物件,其他的都是普通物件。 f1,f2,歸根結底都是通過 new Function()的方式進行建立的。Function Object 也都是通過 New Function()建立的。

5、prototype原型

在JavaScript 中,每當定義一個物件(函式)時候,物件中都會包含一些預定義的屬性。其中函式物件的一個屬性就是原型物件 prototype。注:普通物件沒有prototype,但有_ proto _屬性。

原型物件其實就是普通物件(Function.prototype除外,它是函式物件,但它很特殊,他沒有prototype屬性(前面說道函式物件都有prototype屬性))。看下面的例子:

 function f1(){};
 console.log(f1.prototype) //f1{}
 console.log(typeof f1.prototype) //Object
 console.log(typeof Function.prototype) // Function,這個特殊
 console.log(typeof Object.prototype) // Object
 console.log(typeof Function.prototype.prototype) //undefined

從這句console.log(f1.prototype) //f1 {} 的輸出就結果可以看出,f1.prototype就是f1的一個例項物件(這裡就是f1的原型物件)。就是在f1建立的時候,建立了一個它的例項物件並賦值給它的prototype,基本過程如下:

 var temp = new f1();
 f1. prototype = temp;

所以,Function.prototype為什麼是函式物件就迎刃而解了,上文提到凡是new Function ()產生的物件都是函式物件,所以temp1是函式物件。

var temp1 = new Function ();
 Function.prototype = temp1;

那原型物件是用來做什麼的呢?主要作用是用於繼承。舉了例子:

var person = function(name){
   this.name = name
};
person.prototype.getName = function(){
   return this.name; // 這裡this指向原型物件person ==>person.name
}
var xpg = new person(‘xiaopingguo’);
xpg.getName(); //xiaopingguo

從這個例子可以看出,通過給person.prototype設定了一個函式物件的屬性,那有person例項(例中:xpg)出來的普通物件就繼承了這個屬性。具體是怎麼實現的繼承,就要講到下面的原型鏈了。

在深入的講一遍:無論什麼時候,只要建立了一個新函式,就會根據一組特定的規則為該函式建立一個prototype屬性(同時它也是一個物件),預設情況下prototype屬性(物件)會預設獲得一個constructor(建構函式)屬性,這個屬性是一個指向prototype屬性所在函式的指標,有些繞了啊,寫程式碼、上圖!

function Person(){

}

這裡寫圖片描述

根據上圖可以看出Person物件會自動獲得prototyp屬性,而prototype也是一個物件,會自動獲得一個constructor屬性,該屬性正是指向Person物件。

當呼叫建構函式建立一個例項的時候,例項內部將包含一個內部指標(很多瀏覽器這個指標名字為_ proto _ )指向建構函式的prototype,這個連線存在於例項和建構函式的prototype之間,而不是例項與建構函式之間。

function Person(name){
    this.name=name;
}

Person.prototype.printName=function(){
    alert(this.name);
}

var person1=new Person('Byron');
var person2=new Person('Frank');

這裡寫圖片描述

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

這裡寫圖片描述

寫段程式測試一下看看prototype內屬性、方法是能夠共享

function Person(name){
    this.name=name;
}

Person.prototype.share=[];

Person.prototype.printName=function(){
    alert(this.name);
}

var person1=new Person('Byron');
var person2=new Person('Frank');

person1.share.push(1);
person2.share.push(2);
console.log(person2.share); //[1,2]

果不其然!實際上當程式碼讀取某個物件的某個屬性的時候,都會執行一遍搜尋,目標是具有給定名字的屬性,搜尋首先從物件例項開始,如果在例項中找到該屬性則返回,如果沒有則查詢prototype,如果還是沒有找到則繼續遞迴prototype的prototype物件,直到找到為止,如果遞迴到object仍然沒有則返回錯誤。同樣道理如果在例項中定義如prototype同名的屬性或函式,則會覆蓋prototype的屬性或函式。—-這就是Javascript的原型鏈。

function Person(name){
    this.name=name;
}

Person.prototype.share=[];

var person=new Person('Byron');
person.share=0;

console.log(person.share); //0;而不是prototype中的[]

6.原型鏈

JS在建立物件(不論是普通物件還是函式物件)的時候,都有一個叫做_ proto _的內建屬性,用於指向建立它的函式物件的原型物件prototype。以上面的例子

console.log(xpg.__ proto __ === person.prototype) //true

同樣,person.prototype物件也有_ proto _屬性,它指向建立它的函式物件(Object)的prototype

console.log(person.prototype.__ proto __=== Object.prototype) //true

繼續,Object.prototype物件也有_ proto _屬性,但它比較特殊,為null

console.log(Object.prototype.__ proto __) //null

這個有_ proto _ 串起來的直到Object.prototype._ proto _為null的鏈叫做原型鏈。如下圖:

這裡寫圖片描述

原型鏈中屬性查詢:
當查詢一個物件的屬性時,JavaScript 會向上遍歷原型鏈,直到找到給定名稱的屬性為止,到查詢到達原型鏈的頂部 - 也就是 Object.prototype - 但是仍然沒有找到指定的屬性,就會返回 undefined,我們來看一個例子:

 function foo() {
    this.add = function (x, y) {
        return x + y;
    }
}

foo.prototype.add = function (x, y) {
    return x + y + 10;
}

Object.prototype.subtract = function (x, y) {
    return x - y;
}

var f = new foo();
alert(f.add(1, 2)); //結果是3,而不是13
alert(f.subtract(1, 2)); //結果是-1

通過程式碼執行,我們發現subtract是安裝我們所說的向上查詢來得到結果的,但是add方式有點小不同,這也是我想強調的,就是屬性在查詢的時候是先查詢自身的屬性,如果沒有再查詢原型,再沒有,再往上走,一直插到Object的原型上,所以在某種層面上說,用 for in語句遍歷屬性的時候,效率也是個問題。

還有一點我們需要注意的是,我們可以賦值任何型別的物件到原型上,但是不能賦值原子型別的值,比如如下程式碼是無效的:

function Foo() {}
Foo.prototype = 1; // 無效

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

例項就是通過建構函式建立的。例項一創造出來就具有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>

8、原型的使用

原型使用方式1:
在使用原型之前,我們需要先將程式碼做一下小修改:

    var Calculator = function (decimalDigits, tax) {
        this.decimalDigits = decimalDigits;
        this.tax = tax;
    };

然後,通過給Calculator物件的prototype屬性賦值物件字面量來設定Calculator物件的原型。

 Calculator.prototype = {
            add: function (x, y) {
                return x + y;
            },

            subtract: function (x, y) {
                return x - y;
            }
        };
        //alert((new Calculator()).add(1, 3));

我們就可以new Calculator物件以後,就可以呼叫add方法來計算結果了。

原型使用方式2:
第二種方式是,在賦值原型prototype的時候使用function立即執行的表示式來賦值,即如下格式:

Calculator.prototype = function () { } ();

它的好處在前面的Item裡已經知道了,就是可以封裝私有的function,通過return的形式暴露出簡單的使用名稱,以達到public/private的效果,修改後的程式碼如下:

Calculator.prototype = function () {
    add = function (x, y) {
         return x + y;
     },

     subtract = function (x, y) {
         return x - y;
     }
     return {
         add: add,
         subtract: subtract
     }
} ();

//alert((new Calculator()).add(11, 3));

同樣的方式,我們可以new Calculator物件以後呼叫add方法來計算結果了。

分步宣告:
上述使用原型的時候,有一個限制就是一次性設定了原型物件,我們再來說一下如何分來設定原型的每個屬性吧。

var BaseCalculator = function () {
    //為每個例項都宣告一個小數位數
    this.decimalDigits = 2;
};

//使用原型給BaseCalculator擴充套件

BaseCalculator.prototype.add = function (x, y) {
    return x + y;
};

BaseCalculator.prototype.subtract = function (x, y) {
    return x - y;
};

聲明瞭一個BaseCalculator物件,建構函式裡會初始化一個小數位數的屬性decimalDigits,然後通過原型屬性設定2個function,分別是add(x,y)和subtract(x,y),當然你也可以使用前面提到的2種方式的任何一種,我們的主要目的是看如何將BaseCalculator物件設定到真正的Calculator的原型上。

var BaseCalculator = function() {
    this.decimalDigits = 2;
};

BaseCalculator.prototype = {
    add: function(x, y) {
        return x + y;
    },
    subtract: function(x, y) {
        return x - y;
    }
};

重寫原型:

在使用第三方JS類庫的時候,往往有時候他們定義的原型方法是不能滿足我們的需要,但是又離不開這個類庫,所以這時候我們就需要重寫他們的原型中的一個或者多個屬性或function,我們可以通過繼續宣告的同樣的add程式碼的形式來達到覆蓋重寫前面的add功能,程式碼如下:

//覆蓋前面Calculator的add() function 
Calculator.prototype.add = function (x, y) {
    return x + y + this.tax;
};

var calc = new Calculator();
alert(calc.add(1, 1));

這樣,我們計算得出的結果就比原來多出了一個tax的值,但是有一點需要注意:那就是重寫的程式碼需要放在最後,這樣才能覆蓋前面的程式碼。

9、hasOwnProperty函式

hasOwnProperty是Object.prototype的一個方法,它可是個好東西,他能判斷一個物件是否包含自定義屬性而不是原型鏈上的屬性,因為hasOwnProperty 是 JavaScript 中唯一一個處理屬性但是不查詢原型鏈的函式。

// 修改Object.prototype
Object.prototype.bar = 1; 
var foo = {goo: undefined};

foo.bar; // 1
'bar' in foo; // true

foo.hasOwnProperty('bar'); // false
foo.hasOwnProperty('goo'); // true

只有 hasOwnProperty 可以給出正確和期望的結果,這在遍歷物件的屬性時會很有用。 沒有其它方法可以用來排除原型鏈上的屬性,而不是定義在物件自身上的屬性。

但有個噁心的地方是:JavaScript 不會保護 hasOwnProperty 被非法佔用,因此如果一個物件碰巧存在這個屬性,就需要使用外部的 hasOwnProperty 函式來獲取正確的結果。

var foo = {
    hasOwnProperty: function() {
        return false;
    },
    bar: 'Here be dragons'
};

foo.hasOwnProperty('bar'); // 總是返回 false

// 使用{}物件的 hasOwnProperty,並將其上下為設定為foo
{}.hasOwnProperty.call(foo, 'bar'); // true

當檢查物件上某個屬性是否存在時,hasOwnProperty 是唯一可用的方法。同時在使用 for in loop 遍歷物件時,推薦總是使用 hasOwnProperty 方法,這將會避免原型物件擴充套件帶來的干擾,我們來看一下例子:

// 修改 Object.prototype
Object.prototype.bar = 1;

var foo = {moo: 2};
for(var i in foo) {
    console.log(i); // 輸出兩個屬性:bar 和 moo
}

我們沒辦法改變for in語句的行為,所以想過濾結果就只能使用hasOwnProperty 方法,程式碼如下:

// foo 變數是上例中的
for(var i in foo) {
    if (foo.hasOwnProperty(i)) {
        console.log(i);    //moo
    }
}

這個版本的程式碼是唯一正確的寫法。由於我們使用了 hasOwnProperty,所以這次只輸出 moo。如果不使用 hasOwnProperty,則這段程式碼在原生物件原型(比如 Object.prototype)被擴充套件時可能會出錯。

總結:推薦使用 hasOwnProperty,不要對程式碼執行的環境做任何假設,不要假設原生物件是否已經被擴充套件了

10、拓展

_ ptoto _屬性

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

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

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);//構造屬性,可以獲取建構函式本身,
                        //作用是被原型指標定位,然後得到建構函式本身

_ proto _屬性和prototype屬性的區別

prototype是原型物件中專有的屬性。
_ proto _ 是普通物件的隱式屬性,在new的時候,會指向prototype所指的物件;
_ ptoto _ 實際上是某個實體物件的屬性,而prototype則是屬於建構函式的屬性。_ ptoto _只能在學習或除錯的環境下使用。

原型模式的執行流程

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

原型物件的

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修改

建構函式的

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,就進原則

綜上,整理一下:

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();//例項可以訪問到在原型物件上定義的屬性和方法


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