《JavaScript 模式》讀書筆記(6)— 程式碼複用模式2

Zaking發表於2020-04-19

  上一篇講了最簡單的程式碼複用模式,也是最基礎的,我們普遍知道的繼承模式,但是這種繼承模式卻有不少缺點,我們下面再看看其它可以實現繼承的模式。

四、類式繼承模式#2——借用建構函式

  本模式解決了從子建構函式道父建構函式的引數傳遞問題。本模式借用了父建構函式,它傳遞子物件以繫結到this,並且還轉發任意引數。

function Child(a,c,b,d) {
    parent.apply(this,arguments);
}

  在這種方式中,只能繼承在父建構函式中新增到this的屬性。同時,並不能繼承那些已新增到原型中的成員。

  使用該借用建構函式模式時,子物件獲得了繼承成員的副本,這與類式繼承模式#1中,僅獲取引用的方式是不同的。下面的例子演示了其差異:

// 父建構函式
function Article() {
    this.tags = ['js','css'];
}
var article = new Article();

// blog 文章物件繼承了article物件
// via the classical pattern #1
function BlogPost() {}
BlogPost.prototype = article;
var blog = new BlogPost();

// 注意以上程式碼,你不需要new Article()
// 是因為你已經有一個可用的例項

// static page (靜態頁面)繼承了article
// 通過借用建構函式模式

function StaticPage() {
    Article.call(this);
}

var page = new StaticPage();

console.log(article.hasOwnProperty('tags')); //true
console.log(blog.hasOwnProperty('tags')); //false
console.log(page.hasOwnProperty('tags')); //true

  在以上程式碼片段中,有兩種方式都繼承了父建構函式Article()。預設模式導致了blog物件通過原型以獲得tags屬性的訪問,因此blog物件中沒有將article作為自身的屬性,因此當呼叫hasOwnProperty()時會返回false。相反,page物件本身則具有一個tags屬性,這是由於它在使用借用建構函式的時候,新物件會獲得父物件中tags成員的副本(不是引用)。

  請注意修改繼承的tags屬性時表現出來的差異:

blog.tags.push('html');
page.tags.push('php');
console.log(article.tags.join(', '));// 'js, css, html'

  在上面這個例子中,子物件blog修改了其tags屬性,而這種方式同時也會修改父物件article,這是由於本質上blog.tags和article.tags都指向了同一個陣列。但是,修改page.tags時卻不會影響其父物件article,這是由於在繼承過程中page.tags是獨立建立的一個副本。

 

原型鏈

  當使用本模式以及熟悉的Parent()和Child()建構函式時,讓我們來看原型鏈(prototype chain)的工作流程。其中,Child()需要根據這個新模式的需求略加修改:

// 父建構函式
function Parent(name) {
    this.name = name || 'Adam';
}

// 向該原型新增功能
Parent.prototype.say = function () {
    return this.name;
};

// 子建構函式

function Child(name) {
    Parent.apply(this,arguments);
}

var kid = new Child('Patrick');
console.log(kid.name); // 輸出“Patrick”
console.log(typeof kid.say); //輸出undefined

  如果仔細檢視下圖,將會注意到在new Child物件和Parent物件之間不再有連結。出現這種現象的原因在於本模式中根本就沒有使用Child.prototype,並且它只是指向一個空物件。使用本模式時,kid獲得了自身的屬性name,但是卻從未繼承過say()方法,如果試圖呼叫該方法將會導致錯誤。繼承是一個一次性的操作,它僅會複製父物件的屬性並將其作為子物件自身的屬性,僅此而已。因此,也就不會保留__proto__連結。

 

通過借用建構函式實現多重繼承

  當使用借用建構函式模式時,可以通過借用多個建構函式從而簡單的實現多重繼承。

function Cat() {
    this.legs = 4;
    this.say = function () {
        return "meaowww";
    }
}
function Bird() {
    this.wings = 2;
    this.fly = true;
}

function CatWings() {
    Cat.apply(this);
    Bird.apply(this);
}

var jane = new CatWings();
console.log(jane);

  上述程式碼的執行結果是這樣的:

legs: 4
say: ƒ ()
wings: 2
fly: true

  在解析任意的副本屬性時,將會通過最後一個獲勝的方式來解析該屬性(這句話的意思是,如果複製的屬性中有相同的屬性名,那麼會後者優先)。

 

借用建構函式模式的優缺點

  借用建構函式模式的缺點是很明顯的,如前面所述,其問題在於根本無法從原型中繼承任何東西,並且原型也僅是新增可重用方法以及屬性的位置,它並不會為每個例項重新建立原型。

  本模式的一個優點在於可以獲得父物件自身成員的真實副本,並且也不會存在於子物件意外覆蓋父物件屬性的風險。

  因此,在前面的情況中,如何才能使子物件也能夠繼承原型屬性?以及如何使kid能夠訪問say()方法?下面這個模式將解決這個問題

 

五、類式繼承模式#3——借用和設定原型

  類式繼承模式#3主要思想是結合前兩種模式,即先借用建構函式,然後還設定子建構函式的原型使其指向一個建構函式建立的新例項。如下所示:

function Child(a,c,b,d) {
    Parent.apply(this,arguments);
}

Child.prototype = new Parent()

  這樣做的優點在於,以上程式碼執行後的結果物件能夠獲得父物件本身的成員副本以及指向父物件中可複用功能(以原型成員方式實現的那些功能)的引用。同時,子物件也能夠將任意引數傳遞到父建構函式中。這種行為可能是最接近您希望在Java中實現的方式。可以繼承父物件中的一切東西,同時這種方法也能夠安全的修改自身屬性,且不會帶來修改其父物件的風險。

  這種模式的一個缺點是,父建構函式被呼叫了兩次,因此這導致了其效率低下的問題。最後,自身的屬性(比如本例中扽ame屬性)會被繼承兩次:

function Parent(name) {
    this.name = name || 'Adam';
}

// adding functionality to the prototype
Parent.prototype.say = function () {
    return this.name;
}

// 子建構函式
function Child(name) {
    Parent.apply(this,arguments);
}

Child.prototype = new Parent();

var kid = new Child('Patrick');
console.log(kid.name); //輸出“Patrick”
console.log(kid.say());// 輸出“Patrick”
delete kid.name;
console.log(kid.say());// 輸出“Adam”

  在上面的程式碼中,不同於先前的模式,現在say()方法已被正確的繼承。還可以注意到name屬性卻被繼承了兩次,在我們刪除了kid本身的name屬性的副本後,隨後看到的輸出是原型連結串列現出來所引出的name屬性。

  下圖顯示了物件之間的連結關係。這些關係非常類似於之前#1模式的最後一張圖中所示的原型鏈,但這裡我們所採用的繼承方式是不同的。

 

六、類式繼承模式#4——共享原型

  不同於前面的那種需要兩次呼叫父建構函式的模式(類式繼承模式#3),接下來介紹的模式根本就不涉及呼叫任何父建構函式。

  本模式的經驗法則在於:可複用成員應該轉移到原型中而不是放置在this中。因此,出於繼承的目的,任何值得繼承的東西都應該放置在原型中實現。所以,可以僅將子物件的原型與父物件的原型設定為相同的即可:

function inherit(C, P){
    C.prototype = P.prototype;
}

  這種模式能夠向您提供剪短而迅速的原型鏈查詢,這是由於所有的物件實際上共享了同一個原型。但是,這同時也是一個缺點,因為如果在繼承鏈下方的某處存在一個子物件或者孫子物件修改了原型,它將會影響到所有的父物件和祖先物件。

  如下圖所示,下面的子物件和父物件共享了同一個原型,並且可以同等的訪問say()方法。然而,需要注意到子物件並沒有繼承name屬性。

 

 

七、類式繼承模式#5——臨時建構函式

  類式繼承模式#5通過斷開父物件與子物件的原型之間的直接連結關係,從而解決共享同一個原型所帶來的問題,而且同時還能夠繼續受益於原型鏈帶來的好處。

  下面的程式碼是本模式的一種實現方式,在該程式碼中有一個空白函式F(),該函式充當了子物件與父物件之間的代理。F()的prototype屬性指向父物件的原型。子物件的原型則是一個空白函式例項。

function inherit(C, P){
    var F = function(){};
    F.prototype = P.prototype;
    C.prototype = new F();
}

  這種模式在行為上與預設模式(類式繼承模式#1)略有不同,這是由於這裡的子物件僅繼承了原型的屬性(見下圖)。這種情況通常來說是很好的,實際上也是更加可取的,因為原型也正是放置可複用功能的位置。在這種模式中,父建構函式新增到this中的任何成員都不會被繼承。

  讓我們建立一個新的子物件,並審查其行為:

var kid = new Child();

  如果訪問kid.name,其結果將是undefined型別。在這種情況下,name是父物件所擁有的一個屬性,然而在繼承的時候我們實際上從未呼叫過new Parent(),因此也從未建立過該屬性。當您訪問kid.say()時,在物件#3中該方法並不可用,因此需要開始查詢原型鏈。然而物件#4中也沒有該方法,但是物件#1中確實存在該方法並且位於記憶體中的同一個位置,因此所有繼承了Parent()的不同建構函式,以及所有由其子建構函式所建立的物件都可重用該say()方法。

 

儲存超類

  在上面模式的基礎上,還可以新增一個指向原始父物件的引用。這就像在其他程式語言中訪問超類一樣,這可以偶爾派上用場。

  該屬性被稱之為uber,這僅是由於“super”是保留的關鍵詞,並且“superclass”可能導致存心的程式設計師不加思考便順勢根據該關鍵詞認為JavaScript中具有類(class)。下面是該類式繼承模式的一個改進實現:

function inherit(C, P){
    var F = function(){};
    F.prototype = P.prototype;
    C.prototype = new F();
    C.uber = P.prototype;
}

  

重置建構函式指標

  最後,針對這個幾乎完美的類式繼承函式,還需要做的一件事情就是重置該建構函式的指標,以免在將來的某個時候還需要該建構函式。

  如果不重置該建構函式的指標,那麼所有子物件將會報告Parent()是它們的建構函式,這是沒有任何用處的。因此,使用前面的inherit()實現程式碼,可以觀察到此行為:

// 父子繼承
function Parent() {}
function Child() {}
inherit(Child,Parent);

// 投石問路
var kid = new Child();
console.log(kid.constructor.name); //Parent
console.log(kid.constructor === Parent); //true

  雖然我們很少用到constructor屬性,但是這種功能卻可以很方便的用於執行時物件的內省。可以重置constructor屬性使其指向期望的建構函式且不會影響其功能,這是由於該屬性主要是用於提供物件的資訊。

  這個類式繼承模式最後的聖盃版本看起來如下所示:

function inherit(C, P){
    var F = function(){};
    F.prototype = P.prototype;
    C.prototype = new F();
    C.uber = P.prototype;
    C.prototype.constructor = C;
}

  如果認為這種模式是適用於專案中的最佳方法,需要說明的是,在開源YUI庫或者其他庫中也存在一個與本函式相似的函式,並且它還在沒有類的情況下實現了類式繼承。

  對於該聖盃模式的一個常見優化是避免在每次需要繼承時都建立臨時(代理)建構函式。僅建立一次臨時建構函式,並且修改它的原型,這已經是非常充分的。在具體實現方式上,可以使用即時函式並且在閉包中儲存代理函式。

var inherit = (function () {
    var F = function () { };
    return function (C, P) {
        F.prototype = P.prototype;
        C.prototype = new F();
        C.uber = P.prototype;
        C.prototype.constructor = C;
    }
}());

  

  最基本的類式繼承模式到這裡就告一段落類,但是這遠遠不是結束。

相關文章