JavaScript建立物件(三)——原型模式

bob1900發表於2018-10-18

在JavaScript建立物件(二)——建構函式模式中提到,建構函式模式存在相同功能的函式定義多次的問題。本篇文章就來討論一下該問題的解決方案——原型模式。

首先我們來看下什麼是原型。我們在建立一個函式時,這個函式會包含一個屬性prototype,這個屬性是一個指標,它指向一個物件——該函式的原型物件,這就是原型,它包含了該函式型別的所有例項可共享的屬性和方法,見下面示意圖:

如圖所示,宣告瞭一個函式Person。在JavaScript中,一個函式被宣告的同時就具有了一些屬性,其中有一個叫做prototype,它指向了該函式的原型物件,即上述示例中的Person Prototype。同時,這個原型物件有一個叫做constructor的屬性反過來又指向了該函式物件。

當我們建立一個函式的例項時,例如上面的var personObj = new Person(`張三`, 12);,這個例項也會有一個屬性指向該函式的原型物件,在Chrome的開發工具中顯示為__proto__

上面我們說原型的屬性可以被該函式型別的所有例項所共享,那具體是怎麼實現呢?看下面的示例:

function Person(){
}

//給原型新增自定義屬性和方法
Person.prototype.name = `張三`; 
Person.prototype.sayName = function(){
    console.log(this.name);
}

var p1 = new Person();

//給p1新增age屬性
p1.age = 18;
console.log(p1.name);//張三
console.log(p1.age);//18
p1.sayName();//張三

var p2 = new Person();
console.log(p2.name);//張三
console.log(p2.age);//undefined
p2.sayName();//張三

在上面的程式碼中,我們並沒有給例項新增name屬性和sayName方法,但是依然可以通過例項呼叫,貌似例項天生就具有了原型的屬性和方法,其實不是的,下面是在Chrome的開發工具中看到的內容:

我們看到,p1Person型別的,我們給p1設定了age屬性,這裡也能看到age18。另外我們看到p1有個__proto__屬性,這個就是我們在原型示意圖中說的指向原型物件的屬性。

程式碼讀取物件例項某個屬性的時候會執行一次搜尋,首先搜尋物件例項,如果搜尋到了就返回,如果沒有則會繼續搜尋__proto__指向的原型物件,搜尋到了就返回。所以上面例子中p1.age是搜尋到了p1age屬性返回了,p1.name是搜尋到Person Prototypename返回了,p2.agePerson Prototype中也沒搜到,於是返回了undefined。這就是例項物件共享原型屬性的原理。

除了上面的寫法,原型還有一種更簡單的定義方式,就是用一個包含所有屬性和方法的物件字面量來重寫整個原型物件,這樣避免了每當給原型新增一個屬性就要書寫一遍Person.prototype的繁瑣,同時從視覺上看也更好地封裝了原型的功能,如下程式碼所示:

function Person{
}
Person.prototype = {
    constructor: Person,
    name: `張三`,
    age: 18,
    job: `JavaScript`,
    sayName: function(){
        console.log(this.name);
    }
}

現在回到文章一開始提出的相同功能的函式定義多次的問題,因為函式原型的屬性和方法可以由所有例項所共享,所以只要在原型中定義一次,所有例項就都可以使用,這樣就完美解決了建構函式模式的問題。

總結一下,與建構函式模式相比:

  1. 原型模式不必在建構函式中定義屬性和方法,而是直接定義在原型中。
  2. 這些屬性和方法被所有例項共享。

原型模式雖然好用,但也不是沒有缺點。首先,它省略了為建構函式傳遞初始化引數這一環節,結果所有例項在預設情況下都將取得相同的屬性值。其次,最大的問題是由原型的共享本性帶來的,下面來分析一下原型的共享問題。

通過共享,我們解決了建構函式模式相同功能的函式定義多次的問題,所以共享對於函式是有好處的。對於基本型別的屬性,如上面的nameage,因為屬性的搜尋機制是從例項到原型,所以可以通過給例項新增一個同名的屬性,遮蔽掉原型中相應的屬性,問題也不大。然而,對於引用型別的資料來說,問題就比較嚴重了。來看下面的示例:

function Person(){
}
Person.prototype = {
    constructor: Person,
    name: `張三`,
    age: 18,
    job: `JavaScript`,
    friends: [`小明`, `小剛`],
    sayName: function(){
        console.log(this.name);
    }
}

var p1 = new Person();
var p2 = new Person();
p2.name = `李四`;

p1.friends.push(`小紅`);//張三交了個女朋友小紅
console.log(p1.friends);//["小明", "小剛", "小紅"]
console.log(p2.friends);//["小明", "小剛", "小紅"],我擦,小紅怎麼也成了李四的女朋友
console.log(p1.friends == p2.friends);//true

如上所示,Person.prototype包含了一個引用型別,陣列friends,其中friends只是一個指標,[`小明`, `小剛`]才是真正的物件。通過p1.friends修改了這個陣列,因為共享的問題,p2.friends訪問的也是同一個陣列。假如我們的初衷就是共享一個陣列,那麼也沒問題。但是多數情況下應該是不想共享的場景。比如這裡,張三新交了一個女朋友小紅,結果小紅同時也是李四的女朋友,是張三有這癖好?是小紅劈腿?還是人家張三隻是單純地交了個女朋友,被你搞得複雜了?這個說不清,既然說不清,那麼程式就有問題。所以很少有人單獨使用原型模式,那麼這個問題怎麼解決呢?辦法還是有的,那就是組合使用建構函式模式和原型模式,這個實現也很簡單,但為了區分原型模式,後面將會單獨列一篇文章。

本文參考《JavaScript高階程式設計(第3版)》,關於原型模式的其他特點讀者可以查閱第6.2.3章節,裡面有詳細的說明。


相關文章