【譯】ES6的工廠函數(shù)

原文: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)出了兩個形參稠项,userNameavatar鲜结。這些參數(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ù)支持使用默認值伍绳,這帶來了許多好處:

  1. 開發(fā)者可以通過合適的默認值來省略參數(shù)冲杀。
  2. 默認值提供了期望輸入,提高了函數(shù)自身的可讀性权谁。
  3. 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ù)類型床牧。

沒有默認值遭贸,userName的類型是未知的

通過默認值壕吹,IDE就可以顯示userName預計的輸入是一個字符串。

有了默認值踏堡,userName的類型是string

將函數(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)建一個withConstructormixin函數(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
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末量没,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌蒿柳,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,858評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件妓蛮,死亡現(xiàn)場離奇詭異圾叼,居然都是意外死亡,警方通過查閱死者的電腦和手機构挤,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,372評論 3 395
  • 文/潘曉璐 我一進店門惕鼓,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人矾飞,你說我怎么就攤上這事∪髀伲” “怎么了价淌?”我有些...
    開封第一講書人閱讀 165,282評論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長豺型。 經(jīng)常有香客問我买乃,道長,這世上最難降的妖魔是什么剪验? 我笑而不...
    開封第一講書人閱讀 58,842評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮娶眷,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘烁落。我一直安慰自己豌注,他們只是感情好,可當我...
    茶點故事閱讀 67,857評論 6 392
  • 文/花漫 我一把揭開白布每聪。 她就那樣靜靜地躺著齿风,像睡著了一般。 火紅的嫁衣襯著肌膚如雪救斑。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,679評論 1 305
  • 那天巾陕,我揣著相機與錄音纪他,去河邊找鬼。 笑死梯刚,一個胖子當著我的面吹牛薪寓,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播向叉,決...
    沈念sama閱讀 40,406評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼母谎,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起匹摇,我...
    開封第一講書人閱讀 39,311評論 0 276
  • 序言:老撾萬榮一對情侶失蹤甲葬,失蹤者是張志新(化名)和其女友劉穎绳泉,沒想到半個月后腥椒,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,767評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡驮审,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,945評論 3 336
  • 正文 我和宋清朗相戀三年屿脐,在試婚紗的時候發(fā)現(xiàn)自己被綠了涕蚤。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,090評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡佑钾,死狀恐怖烦粒,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情兽掰,我是刑警寧澤徒役,帶...
    沈念sama閱讀 35,785評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站杉女,受9級特大地震影響鸳吸,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜晌砾,卻給世界環(huán)境...
    茶點故事閱讀 41,420評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望廉白。 院中可真熱鬧,春花似錦猴蹂、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,988評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至漱病,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間杨帽,已是汗流浹背嗤军。 一陣腳步聲響...
    開封第一講書人閱讀 33,101評論 1 271
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留老客,地道東北人震叮。 一個月前我還...
    沈念sama閱讀 48,298評論 3 372
  • 正文 我出身青樓,卻偏偏與公主長得像尉间,于是被迫代替她去往敵國和親钓简。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 45,033評論 2 355

推薦閱讀更多精彩內(nèi)容

  • 第5章 引用類型(返回首頁) 本章內(nèi)容 使用對象 創(chuàng)建并操作數(shù)組 理解基本的JavaScript類型 使用基本類型...
    大學一百閱讀 3,237評論 0 4
  • 前言 人生苦多撤蚊,快來 Kotlin 损话,快速學習Kotlin槽唾! 什么是Kotlin光涂? Kotlin 是種靜態(tài)類型編程...
    任半生囂狂閱讀 26,211評論 9 118
  • 一、ES6簡介 ? 歷時將近6年的時間來制定的新 ECMAScript 標準 ECMAScript 6(亦稱 ...
    一歲一枯榮_閱讀 6,078評論 8 25
  • 每一個男人從小都有一個英雄夢钝计,做一個行俠仗義的江湖大俠齐佳,或劫富濟貧或英雄救美,叱咤于武林本鸣。 都有一個皇帝夢,君臨天...
    煩人的昵稱閱讀 234評論 0 0
  • 記憶里 故鄉(xiāng)的小河依舊潺潺 記憶里 故鄉(xiāng)的天空依舊瓦藍 記憶里 童年的小伙伴還在身邊 記憶里 母親的笑臉依舊安恬 ...
    水晶心語閱讀 408評論 5 9