1. 程式人生 > >漫談JS中的prototype

漫談JS中的prototype

如何使用 函數式 系統 特性 機制 shadow proc http nds

1. 引言

??繼承(inheritance)、封裝(encapsulation)和多態(polymorphism)是面向對象機制的主要特性。在JS中沒有“class”的概念,自然也無法直接進行JAVA、C++常用到的extends、implements等操作。但從某種意義上來說,JS是純粹的“面向對象”編程語言,因為JS中處處皆是對象(函數也是對象),而且作為函數式腳本語言,天生就是多態的。
??網上很多文章探討JS中如何設計class和面向對象機制,這些文章的思路聚焦於如何嚴格按照JAVA、C++中面向對象的實現機制去在JS中實現同樣機制。但在我看來,既然JS中拋去了“class”的定義,就應該充分享受JS的純粹對象機制帶來的便利。

2. 探索prototype

??面向對象中的Class是什麽?其實本質上就是一個“模板”,就像做月餅一樣,我們需要一個月餅模子,而使用月餅模子做出的月餅基本一致。
??那麽在JS中,如何定義“月餅模子”? JS中提供了構造函數,構造函數就是JS中的“月餅模子”。考慮我廠生產月餅的如下代碼:

var 序列號 = 0;
function 我廠月餅模子(廠名,日期){
    this.序列號 = 序列號++;
    this.廠家名字 = 廠名;
    this.生產日期 = 日期;
}

var 我廠月餅1 = new 我廠月餅模子("我廠","20180806");
var 我廠月餅2 = new 我廠月餅模子("我廠","20180807");

??我們在上面的代碼中定義了“我廠月餅模子”這個構造函數(如果使用英文名請將首字母大寫),通過“new”操作做出了兩個月餅:月餅1和月餅2,還在月餅上打上了廠家名字和生產日期。除了沒有出現“class”字樣的關鍵字,這些代碼和JAVA、C++的代碼如此相似(需要註意,JS不支持函數重載,因此相同函數名的構造函數無法被JS重載)。
??我廠月餅生產線運轉起來了。但好景不長,市場是殘酷的,市面上有許多月餅廠家,他們生產的月餅各有特色。我們發現某友商A廠提供未經烘烤的月餅,可由消費者買回家後自己進行烘烤。我們買了一個A廠月餅,它是這樣定義的:

var  A廠月餅1 = {
    月餅形狀: "圓形",
    生產日期: "20180706",
    烘烤: function () {
        console.log("提供烘烤功能");
    }
}

??JS中構造函數具有prototype屬性,當把構造函數“我廠月餅模子”的prototype屬性設置為A廠月餅1後,生產出來的“我廠月餅1”可直接引用prototype對象的屬性,如下代碼所示:

我廠月餅模子.prototype = A廠月餅1;
var 我廠月餅1 = new 我廠月餅模子("我廠", "20180806");
我廠月餅1.烘烤();

??現在我廠生產的月餅也有了烘烤功能了,並且具有形狀特征“圓形”,生產日期為“20180806”。“我廠月餅1”的對象屬性如圖1所示:

技術分享圖片

??圖1可看到對象之間的原型鏈為:我廠月餅1->A廠月餅1->Object->null。
??A廠生產的月餅形狀是可以變化的,可以做成“方形”,也可以做成“圓形”,經過與A廠技術人員交流,我們得到了A廠生產月餅的構造函數如下:

function A廠月餅模子(形狀, 日期) {
    this.月餅形狀 = 形狀;
    this.生產日期 = 日期;
    this.烘烤 = function () {
        console.log("提供烘烤功能");
    }
}

??很明顯,A廠使用這個月餅模子可以做出多種形狀的月餅,按照以前的方法,我們使用一個A廠生產的月餅作為“我廠月餅模子”的prototype,只能固定一個形狀,現在我們也希望在使用“我廠月餅模子”時,可以做出不同形狀的月餅。怎麽辦呢?辦法就是在“我廠月餅模子”中引用“A廠月餅模子”,參考如下代碼:

function 我廠月餅模子(形狀,廠名, 日期) {
    A廠月餅模子.call(this,形狀);
    this.序列號 = 序列號++;
    this.廠家名字 = 廠名;
    this.生產日期 = 日期;
}
var 我廠月餅1 = new 我廠月餅模子("方形","我廠", "20180806");

??再次查看“我廠月餅1”對象,發現已有“方形”這個屬性了。如圖2所示。
技術分享圖片
??與圖1所不同的是,“我廠月餅1”對象的原型鏈已經發生了變化,因為這次,我們沒有使用“我廠月餅模子”的prototype。
??到此為止,似乎一切都已經塵埃落定,我廠不僅保留了原來的月餅特色,還包含了A廠的月餅特色,一切似乎都是那麽的美好。但是不久,我們發現--又出狀況了。A廠生產的月餅提供了DIY配色的功能,用戶能夠根據月餅提供的配色包對月餅進行配色,這一功能頗受部分特定人群的歡迎。
??聯系A廠技術人員,發現他們對“A廠月餅模子”做了修改,在prototype裏增加了配色函數,代碼如下:

A廠月餅模子.prototype.配色 = function(){
    console.log("提供配色功能");
}

??如果我們想繼續共享“A廠月餅模子”的“配色”功能,還是得從prototype來想辦法,這次我們在原來的代碼上將“A廠月餅模子”的prototype設置為一個通用的“A廠月餅模子”生成的“A廠月餅”(構造函數調用時不帶參數),完整代碼如下:

function A廠月餅模子(形狀, 日期) {
    this.月餅形狀 = 形狀;
    this.生產日期 = 日期;
    this.烘烤 = function () {
        console.log("提供烘烤功能");
    }
}

A廠月餅模子.prototype.配色 = function(){
    console.log("提供配色功能");
}

var 序列號 = 0;

function 我廠月餅模子(形狀,廠名, 日期) {
    A廠月餅模子.call(this,形狀);
    this.序列號 = 序列號++;
    this.廠家名字 = 廠名;
    this.生產日期 = 日期;
}

我廠月餅模子.prototype = new A廠月餅模子();
var 我廠月餅1 = new 我廠月餅模子("方形","我廠", "20180806");

??再次查看“我廠月餅1”對象,如圖3所示,已經具有配色的功能(請註意原型鏈已有變化)。
技術分享圖片
??到了現在,終於可以噓一口氣了,A廠再在prototype中增加新功能,我們的代碼不用改了。

3. 使用prototype

??在上節月餅模子的例子中,我們探索了prototype,那麽prototype是一個怎樣的存在,我們來總結一下:
??1. prototype專屬於構造函數,在使用構造函數new出來的對象中,使用proto表示。
??2. prototype對象中包含的屬性(包括函數屬性)被使用構造函數構建的對象所共享。從某種意義上來說,prototype對象就是父對象。
??下面我們根據上節的示例歸納一下不同應用場景下如何使用prototype。為便於描述,我們將需要共享其它對象屬性的對象稱為子對象,生成子對象所使用的構造函數稱為子構造函數,提供共享屬性的對象稱為父對象,生成父對象使用的構造函數稱為父構造函數。
??我們歸納出如下規則:
??1. 若子對象只想共享父構造函數中定義的屬性,在子構造函數調用父構造函數即可,需要註意的是子構造函數的參數可能需要調整。
??2. 若子對象想共享父構造函數和prototype中的所有屬性:當父構造函數無參數時,只需要賦值子構造函數的prototype為使用父構造函數new出來的一個父對象即可;當父構造函數有參數時,不僅要賦值子構造函數的prototype為使用父構造函數new出來的一個父對象(構造函數不帶參數),還需要在子構造函數調用父構造函數(初始化父構造函數中的參數)。
??當我們使用DDD(領域驅動設計)思想來設計軟件時,在建模時我們會設計領域中的實體、值對象和聚合。
??領域中數量最多的應該是實體,這些實體也即編程語言中的對象。設想我們使用JS來編程領域模型,當我們使用“對象共享屬性”的觀點來看待原來的“對象繼承”關系時,也能實現使用JAVA、C++等編程語言達到的效能。

4. 小結

??prototype是JS中常令人迷惑的一個概念,之所以令人迷惑是因為大家總是想把它與面向對象的經典框架結合起來,反而束縛了自己的思維。JS是一個純粹的面向對象系統,使用構造函數的prototype實現了對象屬性間的共享,本文探索了prototype的本質並歸納總結了prototype的使用規則。

漫談JS中的prototype