??繼承(inheritance)、封裝(encapsulation)和多態(tài)(polymorphism)是面向?qū)ο髾C(jī)制的主要特性。在JS中沒有“class”的概念,自然也無法直接進(jìn)行JAVA、C++常用到的extends、implements等操作。但從某種意義上來說,JS是純粹的“面向?qū)ο蟆本幊陶Z言,因?yàn)镴S中處處皆是對(duì)象(函數(shù)也是對(duì)象),而且作為函數(shù)式腳本語言,天生就是多態(tài)的。
?網(wǎng)上很多文章探討JS中如何設(shè)計(jì)class和面向?qū)ο髾C(jī)制,這些文章的思路聚焦于如何嚴(yán)格按照J(rèn)AVA、C++中面向?qū)ο蟮膶?shí)現(xiàn)機(jī)制去在JS中實(shí)現(xiàn)同樣機(jī)制。但在我看來,既然JS中拋去了“class”的定義,就應(yīng)該充分享受JS的純粹對(duì)象機(jī)制帶來的便利。
成都創(chuàng)新互聯(lián)公司堅(jiān)持“要么做到,要么別承諾”的工作理念,服務(wù)領(lǐng)域包括:網(wǎng)站設(shè)計(jì)、成都做網(wǎng)站、企業(yè)官網(wǎng)、英文網(wǎng)站、手機(jī)端網(wǎng)站、網(wǎng)站推廣等服務(wù),滿足客戶于互聯(lián)網(wǎng)時(shí)代的澤州網(wǎng)站設(shè)計(jì)、移動(dòng)媒體設(shè)計(jì)的需求,幫助企業(yè)找到有效的互聯(lián)網(wǎng)解決方案。努力成為您成熟可靠的網(wǎng)絡(luò)建設(shè)合作伙伴!
??面向?qū)ο笾械腃lass是什么?其實(shí)本質(zhì)上就是一個(gè)“模板”,就像做月餅一樣,我們需要一個(gè)月餅?zāi)W?,而使用月餅?zāi)W幼龀龅脑嘛灮疽恢隆?br/>?那么在JS中,如何定義“月餅?zāi)W印保?JS中提供了構(gòu)造函數(shù),構(gòu)造函數(shù)就是JS中的“月餅?zāi)W印薄?紤]我廠生產(chǎn)月餅的如下代碼:
var 序列號(hào) = 0;
function 我廠月餅?zāi)W?廠名,日期){
this.序列號(hào) = 序列號(hào)++;
this.廠家名字 = 廠名;
this.生產(chǎn)日期 = 日期;
}
var 我廠月餅1 = new 我廠月餅?zāi)W?"我廠","20180806");
var 我廠月餅2 = new 我廠月餅?zāi)W?"我廠","20180807");
??我們?cè)谏厦娴拇a中定義了“我廠月餅?zāi)W印边@個(gè)構(gòu)造函數(shù)(如果使用英文名請(qǐng)將首字母大寫),通過“new”操作做出了兩個(gè)月餅:月餅1和月餅2,還在月餅上打上了廠家名字和生產(chǎn)日期。除了沒有出現(xiàn)“class”字樣的關(guān)鍵字,這些代碼和JAVA、C++的代碼如此相似(需要注意,JS不支持函數(shù)重載,因此相同函數(shù)名的構(gòu)造函數(shù)無法被JS重載)。
?我廠月餅生產(chǎn)線運(yùn)轉(zhuǎn)起來了。但好景不長,市場(chǎng)是殘酷的,市面上有許多月餅廠家,他們生產(chǎn)的月餅各有特色。我們發(fā)現(xiàn)某友商A廠提供未經(jīng)烘烤的月餅,可由消費(fèi)者買回家后自己進(jìn)行烘烤。我們買了一個(gè)A廠月餅,它是這樣定義的:
var A廠月餅1 = {
月餅形狀: "圓形",
生產(chǎn)日期: "20180706",
烘烤: function () {
console.log("提供烘烤功能");
}
}
??JS中構(gòu)造函數(shù)具有prototype屬性,當(dāng)把構(gòu)造函數(shù)“我廠月餅?zāi)W印钡膒rototype屬性設(shè)置為A廠月餅1后,生產(chǎn)出來的“我廠月餅1”可直接引用prototype對(duì)象的屬性,如下代碼所示:
我廠月餅?zāi)W?prototype = A廠月餅1;
var 我廠月餅1 = new 我廠月餅?zāi)W?"我廠", "20180806");
我廠月餅1.烘烤();
??現(xiàn)在我廠生產(chǎn)的月餅也有了烘烤功能了,并且具有形狀特征“圓形”,生產(chǎn)日期為“20180806”?!拔覐S月餅1”的對(duì)象屬性如圖1所示:
??圖1可看到對(duì)象之間的原型鏈為:我廠月餅1->A廠月餅1->Object->null。
?A廠生產(chǎn)的月餅形狀是可以變化的,可以做成“方形”,也可以做成“圓形”,經(jīng)過與A廠技術(shù)人員交流,我們得到了A廠生產(chǎn)月餅的構(gòu)造函數(shù)如下:
function A廠月餅?zāi)W?形狀, 日期) {
this.月餅形狀 = 形狀;
this.生產(chǎn)日期 = 日期;
this.烘烤 = function () {
console.log("提供烘烤功能");
}
}
??很明顯,A廠使用這個(gè)月餅?zāi)W涌梢宰龀龆喾N形狀的月餅,按照以前的方法,我們使用一個(gè)A廠生產(chǎn)的月餅作為“我廠月餅?zāi)W印钡膒rototype,只能固定一個(gè)形狀,現(xiàn)在我們也希望在使用“我廠月餅?zāi)W印睍r(shí),可以做出不同形狀的月餅。怎么辦呢?辦法就是在“我廠月餅?zāi)W印敝幸谩癆廠月餅?zāi)W印?,參考如下代碼:
function 我廠月餅?zāi)W?形狀,廠名, 日期) {
A廠月餅?zāi)W?call(this,形狀);
this.序列號(hào) = 序列號(hào)++;
this.廠家名字 = 廠名;
this.生產(chǎn)日期 = 日期;
}
var 我廠月餅1 = new 我廠月餅?zāi)W?"方形","我廠", "20180806");
??再次查看“我廠月餅1”對(duì)象,發(fā)現(xiàn)已有“方形”這個(gè)屬性了。如圖2所示。
?與圖1所不同的是,“我廠月餅1”對(duì)象的原型鏈已經(jīng)發(fā)生了變化,因?yàn)檫@次,我們沒有使用“我廠月餅?zāi)W印钡膒rototype。
?到此為止,似乎一切都已經(jīng)塵埃落定,我廠不僅保留了原來的月餅特色,還包含了A廠的月餅特色,一切似乎都是那么的美好。但是不久,我們發(fā)現(xiàn)--又出狀況了。A廠生產(chǎn)的月餅提供了DIY配色的功能,用戶能夠根據(jù)月餅提供的配色包對(duì)月餅進(jìn)行配色,這一功能頗受部分特定人群的歡迎。
?聯(lián)系A(chǔ)廠技術(shù)人員,發(fā)現(xiàn)他們對(duì)“A廠月餅?zāi)W印弊隽诵薷?,在prototype里增加了配色函數(shù),代碼如下:
A廠月餅?zāi)W?prototype.配色 = function(){
console.log("提供配色功能");
}
??如果我們想繼續(xù)共享“A廠月餅?zāi)W印钡摹芭渖惫δ?,還是得從prototype來想辦法,這次我們?cè)谠瓉淼拇a上將“A廠月餅?zāi)W印钡膒rototype設(shè)置為一個(gè)通用的“A廠月餅?zāi)W印鄙傻摹癆廠月餅”(構(gòu)造函數(shù)調(diào)用時(shí)不帶參數(shù)),完整代碼如下:
function A廠月餅?zāi)W?形狀, 日期) {
this.月餅形狀 = 形狀;
this.生產(chǎn)日期 = 日期;
this.烘烤 = function () {
console.log("提供烘烤功能");
}
}
A廠月餅?zāi)W?prototype.配色 = function(){
console.log("提供配色功能");
}
var 序列號(hào) = 0;
function 我廠月餅?zāi)W?形狀,廠名, 日期) {
A廠月餅?zāi)W?call(this,形狀);
this.序列號(hào) = 序列號(hào)++;
this.廠家名字 = 廠名;
this.生產(chǎn)日期 = 日期;
}
我廠月餅?zāi)W?prototype = new A廠月餅?zāi)W?);
var 我廠月餅1 = new 我廠月餅?zāi)W?"方形","我廠", "20180806");
??再次查看“我廠月餅1”對(duì)象,如圖3所示,已經(jīng)具有配色的功能(請(qǐng)注意原型鏈已有變化)。
?到了現(xiàn)在,終于可以噓一口氣了,A廠再在prototype中增加新功能,我們的代碼不用改了。
??在上節(jié)月餅?zāi)W拥睦又?,我們探索了prototype,那么prototype是一個(gè)怎樣的存在,我們來總結(jié)一下:
?1. prototype專屬于構(gòu)造函數(shù),在使用構(gòu)造函數(shù)new出來的對(duì)象中,使用__proto__表示。
?2. prototype對(duì)象中包含的屬性(包括函數(shù)屬性)被使用構(gòu)造函數(shù)構(gòu)建的對(duì)象所共享。從某種意義上來說,prototype對(duì)象就是父對(duì)象。
?下面我們根據(jù)上節(jié)的示例歸納一下不同應(yīng)用場(chǎng)景下如何使用prototype。為便于描述,我們將需要共享其它對(duì)象屬性的對(duì)象稱為子對(duì)象,生成子對(duì)象所使用的構(gòu)造函數(shù)稱為子構(gòu)造函數(shù),提供共享屬性的對(duì)象稱為父對(duì)象,生成父對(duì)象使用的構(gòu)造函數(shù)稱為父構(gòu)造函數(shù)。
?我們歸納出如下規(guī)則:
?1. 若子對(duì)象只想共享父構(gòu)造函數(shù)中定義的屬性,在子構(gòu)造函數(shù)調(diào)用父構(gòu)造函數(shù)即可,需要注意的是子構(gòu)造函數(shù)的參數(shù)可能需要調(diào)整。
?2. 若子對(duì)象想共享父構(gòu)造函數(shù)和prototype中的所有屬性:當(dāng)父構(gòu)造函數(shù)無參數(shù)時(shí),只需要賦值子構(gòu)造函數(shù)的prototype為使用父構(gòu)造函數(shù)new出來的一個(gè)父對(duì)象即可;當(dāng)父構(gòu)造函數(shù)有參數(shù)時(shí),不僅要賦值子構(gòu)造函數(shù)的prototype為使用父構(gòu)造函數(shù)new出來的一個(gè)父對(duì)象(構(gòu)造函數(shù)不帶參數(shù)),還需要在子構(gòu)造函數(shù)調(diào)用父構(gòu)造函數(shù)(初始化父構(gòu)造函數(shù)中的參數(shù))。
?當(dāng)我們使用DDD(領(lǐng)域驅(qū)動(dòng)設(shè)計(jì))思想來設(shè)計(jì)軟件時(shí),在建模時(shí)我們會(huì)設(shè)計(jì)領(lǐng)域中的實(shí)體、值對(duì)象和聚合。
?領(lǐng)域中數(shù)量最多的應(yīng)該是實(shí)體,這些實(shí)體也即編程語言中的對(duì)象。設(shè)想我們使用JS來編程領(lǐng)域模型,當(dāng)我們使用“對(duì)象共享屬性”的觀點(diǎn)來看待原來的“對(duì)象繼承”關(guān)系時(shí),也能實(shí)現(xiàn)使用JAVA、C++等編程語言達(dá)到的效能。
??prototype是JS中常令人迷惑的一個(gè)概念,之所以令人迷惑是因?yàn)榇蠹铱偸窍氚阉c面向?qū)ο蟮慕?jīng)典框架結(jié)合起來,反而束縛了自己的思維。JS是一個(gè)純粹的面向?qū)ο笙到y(tǒng),使用構(gòu)造函數(shù)的prototype實(shí)現(xiàn)了對(duì)象屬性間的共享,本文探索了prototype的本質(zhì)并歸納總結(jié)了prototype的使用規(guī)則。