ES6的工廠函數
前言
昨天的這個活動福利來了,IMWebConf免費參加了感受到了大家的熱情,竟然留言達到了300多條,但能上榜的評論條數有限,所以蠻多評論都關在小黑屋裡實在抱歉。另外呢昨天@早讀君又完成了今年的一個小KPI,總算結果還是欣慰的。終於時間可以回到正常的軌道上來了。
8月份最後一個周末了,今日早讀來著百度外賣前端團隊@JeLewine童鞋授權分享。
正文從這開始~
工廠函數是一個最後返回值是對象的函數,但它既不是類,也不是構造函數。在JavaScript中,任何函數都可以返回一個對象。但當函數沒有使用new關鍵字時,那它便是一個工廠函數。
由於工廠函數提供了讓我們輕鬆創建對象實例的能力,而且還不需要深入學習類和new關鍵字的複雜性。因此工廠函數在JavaScript中是非常具有吸引力的
JavaScript提供了非常方便的對象字面量語法。就像下面這樣:
constuser={
userName: echo ,
avatar: echo.png
}
這很像是JSON,:左邊是屬性名,右邊是屬性值。你可以輕鬆的使用點符號來訪問屬性:
console.log(user.userName);// "echo"
你也可以通過方括弧語法來訪問屬性:
constkey= avatar ;
console.log(user[key]);// "echo.png"
如果在作用域內還有變數和你想要創建的對象屬性名相同,那你也可以直接使用這一變數來創建對象字面量:
constuserName= echo ;
constavatar= echo.png ;
constuser={
userName,
avatar
};
console.log(user);
// { "avatar": "echo.png", "userName": "echo" }
對象字面量有著簡單明了的函數語法。我們可以給對象添加一個.setUserName()方法:
constuserName= echo ;
constavatar= echo.png ;
constuser={
userName,
avatar,
setUserName(userName){
this.userName=userName;
returnthis;
}
};
console.log(user.setUserName( Foo ).userName);// "Foo"
在這個方法中,this指向調用這個方法的對象。想要在一個對象中調用方法,只需要使用對象的點語法或者利用方括弧語法就行了。像是game.play()將會在game中調用play()。不過使用點語法來進行調用是有前提的,就是這個方法必須是這個對象的屬性。不過你也可以利用.call(),.apply(),.bind()來將一個方法應用在任意對象上。
在這個例子中,user.setUserName( Foo )是在`user`對象上調用.setUserName(),所以this === user。在.setUserName()方法中,通過this綁定,我們修改了user對象上的.userName屬性。同時為了方便鏈式調用,它返回了相同的一個對象實例。
字面量語法針對單一對象,工廠函數更適合多對象創建
如果你需要創建許多對象,我覺得你會十分想把對象字面量創建與工廠函數結合起來。
只要你想,你可以用工廠函數創建任意多的`user`對象。舉個栗子,如果你正在開發一個聊天app,你就可以創建一個對象來代表當前的用戶。你同時還可以創建很多其它對象來代表其他那些已經登陸或者在聊天的用戶,以方便來展示他們的名字和頭像。
讓我們把我們的user對象用createUser()工廠函數造出來:
constcreateUser=({userName,avatar})=>({
userName,
avatar,
setUserName(userName){
this.userName=userName;
returnthis;
}
});
console.log(createUser({userName: echo ,avatar: echo.png }));
/*
{
"avatar": "echo.png",
"userName": "echo",
"setUserName": [Function setUserName]
}
*/
返回對象
箭頭函數(=>)具有隱式返回的特性。如果某個函數體只有單個表達式,你就可以忽略return關鍵字:() => foo是一個不需要參數,而且最後會返回字元串 foo 的的函數。
不過需要注意的是,當你想要返回一個對象字面量的時候,如果你使用了大括弧,JavaScript會默認你想要創建一個函數體。像是{ broken: true }。如果你想要通過隱式返回來返回一個字面量對象,那你就需要在你的字面量對象外面包裹一層小括弧來消除這種歧義:
constnoop=()=>{foo: bar };
console.log(noop());// undefined
constcreateFoo=()=>({foo: bar });
console.log(createFoo());// { foo: "bar" }
在第一個栗子中,foo:會被JavaScript理解成一個標籤,而bar會被理解成一個沒有被賦值的表達式。這個函數會返回undefined。
而在createFoo()的栗子中,圓括弧強制讓大括弧里的內容被解釋為一個需要被計算的表達式,而不是一個函數體。
解構
需要特別注意一下函數的聲明:
constcreateUser=({userName,avatar})=>({
在這一行中,大括弧({,})代表了對象的解構。這個函數接受一個參數(一個對象),但是從這個單一對象中又解構出了兩個形參,userName和avatar。這些參數都可以被當作函數體作用域內的變數使用。你同樣也可以解構一些數組:
constswap=([first,second])=>[second,first];
console.log(swap([1,2]));// [2, 1]
你也可以使用拓展運算符(...varName)來獲取數組(或者參數列表)中的其它值,然後將這些數組元素回傳成單個元素:
constrotate=([first,...rest])=>[...rest,first];
console.log(rotate([1,2,3]));// [2, 3, 1]
計算屬性值
在前面我們曾通過方括弧語法來動態的訪問對象的屬性:
constkey= avatar ;
console.log(user[key]);// "echo.png"
我們也可以將計算到的屬性值指定給某些對象:
constarrToObj=([key,value])=>({[key]:value});
console.log(arrToObj([ foo , bar ]));// { "foo": "bar" }
在這個栗子里,arrToObj將一個包含鍵值對(也叫元組)的數組轉換成了一個對象。因為我們不知道鍵的名稱,所以我們需要通過計算屬性名來在對象中設置鍵值對。為此,我們借用了計算屬性中方括弧語法的思路來重建我們的對象字面量。
{[key]:value}
在語句解析完成後,我們就得到了我們最終的對象:
{"foo":"bar"}
默認參數
JavaScript函數支持使用默認值,這帶來了許多好處:
開發者可以通過合適的默認值來省略參數。
默認值提供了期望輸入,提高了函數自身的可讀性。
IDE和靜態檢測可以通過默認值來推測參數的類型。舉個栗子,默認值為1表明參數可能是Number類型的。
使用默認參數,就好像是給我們的createUser工廠函數提供了一份期待介面文檔。如果user沒有提供任何信息,函數就會自動設為默認值Anonymous。
constcreateUser=({
userName= Anonymous ,
avatar= anon.png
}={})=>({
userName,
avatar
});
console.log(
// { userName: "echo", avatar: anon.png }
createUser({userName: echo }),
// { userName: "Anonymous", avatar: anon.png }
createUser()
);
函數定義的最後一部分看起來有點意思:
在傳參結束之前的最後的這部分`={}`用於表示:如果沒有傳入任何參數,那麼將使用一個空對象作為默認值傳入函數體。當你嘗試從空對象中解構對象時,將會自動使用屬性的默認值。因為這就是默認值乾的事:用預設的值(空對象)來替換undefined。
如果沒有={}這部分,createUser()就會報錯。因為你無法訪問undefined的屬性值。
類型判斷
在我還在寫這篇文章的時候,JavaScript 還沒有任何原生的類型注釋。但是近幾年湧現了一批工具填補這一空白,包括JSDoc(由於出現了更好的選擇,所以它現在呈現出了下降趨勢),Facebook的Flow,還有Microsoft的TypeScript。我個人比較喜歡rtype,因為我覺得它在函數式編程方面比TypeScript擁有更好的可讀性。
至少到我發文的時候,類型注釋好像還沒有一個明確的贏家。沒有一個獲得了JavaScript規範的支持,而且每一個選項似乎都有著較為明顯的缺點。
類型判斷是基於我們使用的變數上下文來判斷類型的過程。在JavaScript中,類型注釋是一個非常好的選擇。
如果你可以在標準的JavaScript函數簽名中提供足夠多的線索,那麼你就可以獲得類型注釋的絕大部分好處,而不用擔心什麼代價和風險。
即使你打算使用類似typescript或flow這樣的工具,也應該儘可能的帶上類型注釋。這樣可以減少一些情況下發生的強類型判斷。比方說,原生JavaScript是不支持定義介面的。但是使用typescript和flow都可以方便的定義介面。
Tern.js是一個流行的JavaScript類型判斷工具,它在很多代碼編輯器或IDE上都有插件。
微軟的VS Code並不需要Tern,因為它已經把TypeScript的類型判斷功能加到了普通的JavaScript代碼中去了。
當你在JavaScript函數中指定了默認參數值後,很多類型判斷工具就已經可以在IDE中給予你提示,來幫助正確的使用API了。
沒有默認值,各種IDE(更多時候,甚至連我們自己)沒有足夠的信息來判斷函數預期的參數類型。
沒有默認值,userName的類型是未知的
通過默認值,IDE就可以顯示userName預計的輸入是一個字元串。
有了默認值,userName的類型是string
將函數參數限制為固定類型(這會使通用函數和高階函數更加受限)並不總是合理的。但在它合理的時候,使用默認參數通常就是最佳的方式。即便你已經在使用TypeScript或Flow做類型判斷了。
Mixin結構的工廠函數
工廠函數擅長利用封裝好的API來創建對象。通常來說,這已經足夠了。但是不久你就會發現,你總是需要將許多相似的功能構築到不同類型的對象中去。你會想要把這些功能抽象為mixin函數,來進行重用。
這正是mixin函數將要大顯身手的地方。我們來創建一個withConstructor mixin函數,把.constructor屬性添加到所有對象實例當中去。
with-constructor.js
constwithConstructor=constructor=>o=>{
constproto=Object.assign({},
Object.getPrototypeOf(o),
{constructor}
);
returnObject.assign(Object.create(proto),o);
};
現在你可以在其它mixin函數中使用它了
importwithConstructorfrom ./with-constructor ;
constpipe=(...fns)=>x=>fns.reduce((y,f)=>f(y),x);
// or `import pipe from lodash/fp/flow ;`
// Set up some functional mixins
constwithFlying=o=>{
letisFlying=false;
return{
...o,
fly(){
isFlying=true;
returnthis;
},
land(){
isFlying=false;
returnthis;
},
isFlying:()=>isFlying
}
};
constwithBattery=({capacity})=>o=>{
letpercentCharged=100;
return{
...o,
draw(percent){
constremaining=percentCharged-percent;
percentCharged=remaining>?remaining:;
returnthis;
},
getCharge:()=>percentCharged,
getcapacity(){
returncapacity
}
};
};
constcreateDrone=({capacity= 3000mAh })=>pipe(
withFlying,
withBattery({capacity}),
withConstructor(createDrone)
)({});
constmyDrone=createDrone({capacity: 5500mAh });
console.log(`
can fly:${myDrone.fly().isFlying()===true}
can land:${myDrone.land().isFlying()===false}
battery capacity:${myDrone.capacity}
battery status:${myDrone.draw(50).getCharge()}%
battery drained:${myDrone.draw(75).getCharge()}%
`);
console.log(`
constructor linked:${myDrone.constructor===createDrone}
`);
如你所見,可復用的withConstructor() mixin非常輕鬆的和其它mixin一起放進pipeline中。withBattery()可以被用在其它各種類型的對象上:機器人、電動滑板或是充電寶等等。withFlying()也可以被用在飛行模型、火箭和熱氣球身上。
對象組合更像是一種思維方式,而不是一種簡單的編程技巧。你可以用各種各樣的方式來實現它。而函數組合正是從頭開始構建這種思維方式的最簡單的方法。工廠函數是最簡單的能夠將實現細節封裝成對外友好API的方法。
結論
ES6提供了非常方便的語法來處理對象創建和工廠函數。在大多數情況下,這已經可以滿足你絕大多數的需求。不過還有一種更像Java的方式:利用class關鍵字。
在JavaScript中,類比工廠模式更加的冗餘和受限。而且如果要涉及重構的話,這更像是一塊兒雷區。不過類也被當前的一些像是React和Angular的主流前端框架所接受。還有一些其它的罕見情況也讓類的存在更加有意義。
「有時候,最優雅的實現僅僅需要一個函數。不是類,不是方法,也不是框架。僅僅是一個函數。」 ~ John Carmack
從最簡單的實現開始,根據需要去改變成更加複雜的實現方式。當涉及到對象時,整個變化過程看起來就像這樣:
Purefunction->factory->functional mixin->class
關於本文
譯者:@JeLewine
作者:@Eric Elliott
原文:https://medium.com/javascript-scene/javascript-factory-functions-with-es6-4d224591a8b1
點擊展開全文
※福利來了,IMWebConf免費參加了
※杭州網易農業事業部尋找資深前端
※美團點評點餐前後端分離實踐
※前端的人真的這麼多嗎?
※如何在 Webpack 2 中使用 tree-shaking
TAG:前端早讀課 |
※NEC 規劃裁員 3,000 名集團員工,並關閉部分工廠
※三星13億美元的7nm工廠完工,7nm EUV工藝量產提速
※圖解:亞洲工廠組裝的 VESPA GTS SUPER 300
※NEC計劃裁員3000人,並關閉部分工廠
※RX 590顯卡GPU核心存兩家代工廠:GF 12nm和三星11nm
※SK Hynix開建第七座晶元工廠:投資15萬億韓元,EUV光刻工藝
※SK海力士投資3.5兆韓元在利川建M16工廠,生產DRAM?
※JOLED 向 JDI 買工廠,2020 年量產印刷式 OLED
※增產3D NAND!東芝擬再建新工廠 預計2019年完工
※抗韓廠!JOLED向JDI買工廠、2020年量產印刷式OLED
※無錫二工廠正式竣工,SK海力士或將中國45% DRAM市場收入囊中
※78官評:變形金剛第三方 鐵工廠 IF EX-24X 禍根
※三星全力突擊7nmEUV技術 台積電5nm工廠今年完工
※SKC子公司HMT將在蘇州投資340億投資建設專用膜片工廠
※380億!三星7nm EUV工廠破土動工:新驍龍將在這裡誕生
※對抗韓廠!JOLED購買JDI工廠,2020年量產印刷式OLED
※英特爾大連工廠二期投產:主要生產96層3D NAND
※MKS工廠朗格經典1815系列腕錶評測賞析
※Nvidia下一代GPU生產將換代工廠,採用三星的7nm EUV工藝
※iPhone銷售降、JDI白山工廠稼動率近零?傳停工3個月