原文:JavaScript Factory Functions with ES6+
作者:Eric Elliott
譯者:JeLewine
工廠函數(shù)是一個最后返回值是對象的函數(shù),但它既不是類沾乘,也不是構(gòu)造函數(shù)翅阵。在JavaScript中,任何函數(shù)都可以返回一個對象滥崩。但當函數(shù)沒有使用new關(guān)鍵字時讹语,那它便是一個工廠函數(shù)。
由于工廠函數(shù)提供了讓我們輕松創(chuàng)建對象實例的能力株灸,而且還不需要深入學習類和new
關(guān)鍵字的復雜性擎值。因此工廠函數(shù)在JavaScript中是非常具有吸引力的
JavaScript提供了非常方便的對象字面量語法逐抑。就像下面這樣:
const user = {
userName: 'echo',
avatar: 'echo.png'
};
這很像是JSON厕氨,:
左邊是屬性名,右邊是屬性值命斧。你可以輕松的使用點符號來訪問屬性:
console.log(user.userName); // "echo"
你也可以通過方括號語法來訪問屬性:
const key = 'avatar';
console.log( user[key] ); // "echo.png"
如果在作用域內(nèi)還有變量和你想要創(chuàng)建的對象屬性名相同国葬,那你也可以直接使用這一變量來創(chuàng)建對象字面量:
const userName = 'echo';
const avatar = 'echo.png';
const user = {
userName,
avatar
};
console.log(user);
// { "avatar": "echo.png", "userName": "echo" }
對象字面量有著簡單明了的函數(shù)語法。我們可以給對象添加一個.setUserName()
方法:
const userName = 'echo';
const avatar = 'echo.png';
const user = {
userName,
avatar,
setUserName (userName) {
this.userName = userName;
return this;
}
};
console.log(user.setUserName('Foo').userName); // "Foo"
在這個方法中接奈,this
指向調(diào)用這個方法的對象通孽。想要在一個對象中調(diào)用方法背苦,只需要使用對象的點語法或者利用方括號語法就行了潘明。像是game.play()
將會在game
中調(diào)用play()
秕噪。不過使用點語法來進行調(diào)用是有前提的,就是這個方法必須是這個對象的屬性牲阁。不過你也可以利用.call()
壤躲,.apply()
碉克,.bind()
來將一個方法應用在任意對象上。
在這個例子中漏麦,user.setUserName('Foo')
是在user
對象上調(diào)用.setUserName()
撕贞,所以this === user
。在.setUserName()
方法中秧均,通過this
綁定号涯,我們修改了user
對象上的.userName
屬性。同時為了方便鏈式調(diào)用誉己,它返回了相同的一個對象實例域蜗。
字面量語法針對單一對象霉祸,工廠函數(shù)更適合多對象創(chuàng)建
如果你需要創(chuàng)建許多對象,我覺得你會十分想把對象字面量創(chuàng)建與工廠函數(shù)結(jié)合起來脉执。
只要你想,你可以用工廠函數(shù)創(chuàng)建任意多的user
對象婆廊。舉個栗子淘邻,如果你正在開發(fā)一個聊天app,你就可以創(chuàng)建一個對象來代表當前的用戶统阿。你同時還可以創(chuàng)建很多其它對象來代表其他那些已經(jīng)登陸或者在聊天的用戶筹我,以方便來展示他們的名字和頭像。
讓我們把我們的user
對象用createUser()
工廠函數(shù)造出來:
const createUser = ({ userName, avatar }) => ({
userName,
avatar,
setUserName (userName) {
this.userName = userName;
return this;
}
});
console.log(createUser({ userName: 'echo', avatar: 'echo.png' }));
/*
{
"avatar": "echo.png",
"userName": "echo",
"setUserName": [Function setUserName]
}
*/
返回對象
箭頭函數(shù)(=>
)具有隱式返回的特性结澄。如果某個函數(shù)體只有單個表達式岸夯,你就可以忽略return
關(guān)鍵字:() => foo
是一個不需要參數(shù)猜扮,而且最后會返回字符串'foo'
的的函數(shù)。
不過需要注意的是齿桃,當你想要返回一個對象字面量的時候鲜漩,如果你使用了大括號孕似,JavaScript會默認你想要創(chuàng)建一個函數(shù)體喉祭。像是{ broken: true }
泛烙。如果你想要通過隱式返回來返回一個字面量對象翘紊,那你就需要在你的字面量對象外面包裹一層小括號來消除這種歧義:
const noop = () => { foo: 'bar' };
console.log(noop()); // undefined
const createFoo = () => ({ foo: 'bar' });
console.log(createFoo()); // { foo: "bar" }
在第一個栗子中,foo:
會被JavaScript理解成一個標簽鹉究,而bar
會被理解成一個沒有被賦值的表達式自赔。這個函數(shù)會返回undefined
。
而在createFoo()
的栗子中润脸,圓括號強制讓大括號里的內(nèi)容被解釋為一個需要被計算的表達式他去,而不是一個函數(shù)體。
解構(gòu)
需要特別注意一下函數(shù)的聲明:
const createUser = ({ userName, avatar }) => ({
在這一行中尔苦,大括號({
,}
)代表了對象的解構(gòu)行施。這個函數(shù)接受一個參數(shù)(一個對象),但是從這個單一對象中又解構(gòu)出了兩個形參稠项,userName
和avatar
鲜结。這些參數(shù)都可以被當作函數(shù)體作用域內(nèi)的變量使用精刷。你同樣也可以解構(gòu)一些數(shù)組:
const swap = ([first, second]) => [second, first];
console.log( swap([1, 2]) ); // [2, 1]
你也可以使用拓展運算符(...varName
)來獲取數(shù)組(或者參數(shù)列表)中的其它值,然后將這些數(shù)組元素回傳成單個元素:
const rotate = ([first, ...rest]) => [...rest, first];
console.log( rotate([1, 2, 3]) ); // [2, 3, 1]
計算屬性值
在前面我們曾通過方括號語法來動態(tài)的訪問對象的屬性:
const key = 'avatar';
console.log( user[key] ); // "echo.png"
我們也可以將計算到的屬性值指定給某些對象:
const arrToObj = ([key, value]) => ({ [key]: value });
console.log( arrToObj([ 'foo', 'bar' ]) ); // { "foo": "bar" }
在這個栗子里埂软,arrToObj
將一個包含鍵值對(也叫元組)的數(shù)組轉(zhuǎn)換成了一個對象勘畔。因為我們不知道鍵的名稱丽惶,所以我們需要通過計算屬性名來在對象中設(shè)置鍵值對。為此万哪,我們借用了計算屬性中方括號語法的思路來重建我們的對象字面量。
{ [key]: value }
在語句解析完成后陵霉,我們就得到了我們最終的對象:
{ "foo": "bar" }
默認參數(shù)
JavaScript函數(shù)支持使用默認值伍绳,這帶來了許多好處:
- 開發(fā)者可以通過合適的默認值來省略參數(shù)冲杀。
- 默認值提供了期望輸入,提高了函數(shù)自身的可讀性权谁。
- IDE和靜態(tài)檢測可以通過默認值來推測參數(shù)的類型旺芽。舉個栗子,默認值為
1
表明參數(shù)可能是Number類型的运嗜。
使用默認參數(shù)悯舟,就好像是給我們的createUser
工廠函數(shù)提供了一份期待接口文檔抵怎。如果user沒有提供任何信息,函數(shù)就會自動設(shè)為默認值Anonymous
反惕。
const createUser = ({
userName = 'Anonymous',
avatar = 'anon.png'
} = {}) => ({
userName,
avatar
});
console.log(
// { userName: "echo", avatar: 'anon.png' }
createUser({ userName: 'echo' }),
// { userName: "Anonymous", avatar: 'anon.png' }
createUser()
);
函數(shù)定義的最后一部分看起來有點意思:
} = {}) => ({
在傳參結(jié)束之前的最后的這部分={}
用于表示:如果沒有傳入任何參數(shù)姿染,那么將使用一個空對象作為默認值傳入函數(shù)體。當你嘗試從空對象中解構(gòu)對象時隘梨,將會自動使用屬性的默認值舷嗡。因為這就是默認值干的事:用預設(shè)的值(空對象)來替換undefined
进萄。
如果沒有={}
這部分,createUser()
就會報錯可婶。因為你無法訪問undefined
的屬性值援雇。
類型判斷
在我還在寫這篇文章的時候,JavaScript 還沒有任何原生的類型注釋具温。但是近幾年涌現(xiàn)了一批工具填補這一空白筐赔,包括JSDoc(由于出現(xiàn)了更好的選擇茴丰,所以它現(xiàn)在呈現(xiàn)出了下降趨勢),F(xiàn)acebook的Flow峦椰,還有Microsoft的TypeScript尸曼。我個人比較喜歡rtype,因為我覺得它在函數(shù)式編程方面比TypeScript擁有更好的可讀性冤竹。
至少到我發(fā)文的時候茬射,類型注釋好像還沒有一個明確的贏家在抛。沒有一個獲得了JavaScript規(guī)范的支持,而且每一個選項似乎都有著較為明顯的缺點肠阱。
類型判斷是基于我們使用的變量上下文來判斷類型的過程朴读。在JavaScript中衅金,類型注釋是一個非常好的選擇簿煌。
如果你可以在標準的JavaScript函數(shù)簽名中提供足夠多的線索鉴吹,那么你就可以獲得類型注釋的絕大部分好處豆励,而不用擔心什么代價和風險。
即使你打算使用類似typescript或flow這樣的工具般堆,也應該盡可能的帶上類型注釋诚啃。這樣可以減少一些情況下發(fā)生的強類型判斷始赎。比方說,原生JavaScript是不支持定義接口的魔招。但是使用typescript和flow都可以方便的定義接口五辽。
Tern.js是一個流行的JavaScript類型判斷工具,它在很多代碼編輯器或IDE上都有插件乡翅。
微軟的VS Code并不需要Tern罪郊,因為它已經(jīng)把TypeScript的類型判斷功能加到了普通的JavaScript代碼中去了悔橄。
當你在JavaScript函數(shù)中指定了默認參數(shù)值后,很多類型判斷工具就已經(jīng)可以在IDE中給予你提示挣柬,來幫助正確的使用API了睛挚。
沒有默認值竞川,各種IDE(更多時候,甚至連我們自己)沒有足夠的信息來判斷函數(shù)預期的參數(shù)類型床牧。
通過默認值壕吹,IDE就可以顯示userName
預計的輸入是一個字符串。
將函數(shù)參數(shù)限制為固定類型(這會使通用函數(shù)和高階函數(shù)更加受限)并不總是合理的顷蟆。但在它合理的時候腐魂,使用默認參數(shù)通常就是最佳的方式蛔屹。即便你已經(jīng)在使用TypeScript或Flow做類型判斷了。
Mixin結(jié)構(gòu)的工廠函數(shù)
工廠函數(shù)擅長利用封裝好的API來創(chuàng)建對象漫贞。通常來說育叁,這已經(jīng)足夠了绕辖。但是不久你就會發(fā)現(xiàn),你總是需要將許多相似的功能構(gòu)筑到不同類型的對象中去擂红。你會想要把這些功能抽象為mixin函數(shù)仪际,來進行重用。
這正是mixin函數(shù)將要大顯身手的地方昵骤。我們來創(chuàng)建一個withConstructor
mixin函數(shù)树碱,把.constructor
屬性添加到所有對象實例當中去。
with-constructor.js
const withConstructor = constructor => o => {
const proto = Object.assign({},
Object.getPrototypeOf(o),
{ constructor }
);
return Object.assign(Object.create(proto), o);
};
現(xiàn)在你可以在其它mixin函數(shù)中使用它了
import withConstructor from `./with-constructor';
const pipe = (...fns) => x => fns.reduce((y, f) => f(y), x);
// or `import pipe from 'lodash/fp/flow';`
// Set up some functional mixins
const withFlying = o => {
let isFlying = false;
return {
...o,
fly () {
isFlying = true;
return this;
},
land () {
isFlying = false;
return this;
},
isFlying: () => isFlying
}
};
const withBattery = ({ capacity }) => o => {
let percentCharged = 100;
return {
...o,
draw (percent) {
const remaining = percentCharged - percent;
percentCharged = remaining > 0 ? remaining : 0;
return this;
},
getCharge: () => percentCharged,
get capacity () {
return capacity
}
};
};
const createDrone = ({ capacity = '3000mAh' }) => pipe(
withFlying,
withBattery({ capacity }),
withConstructor(createDrone)
)({});
const myDrone = 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()
也可以被用在飛行模型刘绣、火箭和熱氣球身上挣输。
對象組合更像是一種思維方式,而不是一種簡單的編程技巧撩嚼。你可以用各種各樣的方式來實現(xiàn)它。而函數(shù)組合正是從頭開始構(gòu)建這種思維方式的最簡單的方法完丽。工廠函數(shù)是最簡單的能夠?qū)崿F(xiàn)細節(jié)封裝成對外友好API的方法恋技。
結(jié)論
ES6提供了非常方便的語法來處理對象創(chuàng)建和工廠函數(shù)。在大多數(shù)情況下蜻底,這已經(jīng)可以滿足你絕大多數(shù)的需求。不過還有一種更像Java的方式:利用class
關(guān)鍵字朱躺。
在JavaScript中,類比工廠模式更加的冗余和受限搁痛。而且如果要涉及重構(gòu)的話长搀,這更像是一塊兒雷區(qū)。不過類也被當前的一些像是React和Angular的主流前端框架所接受鸡典。還有一些其它的罕見情況也讓類的存在更加有意義。
“有時候谁尸,最優(yōu)雅的實現(xiàn)僅僅需要一個函數(shù)纽甘。不是類,不是方法悍赢,也不是框架。僅僅是一個函數(shù)左权∩统伲” ~ John Carmack
從最簡單的實現(xiàn)開始,根據(jù)需要去改變成更加復雜的實現(xiàn)方式。當涉及到對象時泻仙,整個變化過程看起來就像這樣:
Pure function -> factory -> functional mixin -> class