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

原文:JavaScript Factory Functions with ES6+
作者:Eric Elliott
譯者:JeLewine

工廠函數(shù)是一個最后返回值是對象的函數(shù),但它既不是類,也不是構(gòu)造函數(shù)。在JavaScript中,任何函數(shù)都可以返回一個對象。但當(dāng)函數(shù)沒有使用new關(guān)鍵字時,那它便是一個工廠函數(shù)。

由于工廠函數(shù)提供了讓我們輕松創(chuàng)建對象實例的能力,而且還不需要深入學(xué)習(xí)類和new關(guān)鍵字的復(fù)雜性。因此工廠函數(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()來將一個方法應(yīng)用在任意對象上。

在這個例子中,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)建一個對象來代表當(dā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ù)。

不過需要注意的是,當(dāng)你想要返回一個對象字面量的時候,如果你使用了大括號,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理解成一個標(biāo)簽,而bar會被理解成一個沒有被賦值的表達式。這個函數(shù)會返回undefined。

而在createFoo()的栗子中,圓括號強制讓大括號里的內(nèi)容被解釋為一個需要被計算的表達式,而不是一個函數(shù)體。

解構(gòu)

需要特別注意一下函數(shù)的聲明:

const createUser = ({ userName, avatar }) => ({

在這一行中,大括號({,})代表了對象的解構(gòu)。這個函數(shù)接受一個參數(shù)(一個對象),但是從這個單一對象中又解構(gòu)出了兩個形參,userNameavatar。這些參數(shù)都可以被當(dāng)作函數(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ù)體。當(dāng)你嘗試從空對象中解構(gòu)對象時,將會自動使用屬性的默認值。因為這就是默認值干的事:用預(yù)設(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中,類型注釋是一個非常好的選擇。

如果你可以在標(biāo)準的JavaScript函數(shù)簽名中提供足夠多的線索,那么你就可以獲得類型注釋的絕大部分好處,而不用擔(dān)心什么代價和風(fēng)險。

即使你打算使用類似typescript或flow這樣的工具,也應(yīng)該盡可能的帶上類型注釋。這樣可以減少一些情況下發(fā)生的強類型判斷。比方說,原生JavaScript是不支持定義接口的。但是使用typescript和flow都可以方便的定義接口。

Tern.js是一個流行的JavaScript類型判斷工具,它在很多代碼編輯器或IDE上都有插件。

微軟的VS Code并不需要Tern,因為它已經(jīng)把TypeScript的類型判斷功能加到了普通的JavaScript代碼中去了。

當(dāng)你在JavaScript函數(shù)中指定了默認參數(shù)值后,很多類型判斷工具就已經(jīng)可以在IDE中給予你提示,來幫助正確的使用API了。

沒有默認值,各種IDE(更多時候,甚至連我們自己)沒有足夠的信息來判斷函數(shù)預(yù)期的參數(shù)類型。

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

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

有了默認值,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屬性添加到所有對象實例當(dāng)中去。
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 }
`);

如你所見,可復(fù)用的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ū)。不過類也被當(dāng)前的一些像是React和Angular的主流前端框架所接受。還有一些其它的罕見情況也讓類的存在更加有意義。

“有時候,最優(yōu)雅的實現(xiàn)僅僅需要一個函數(shù)。不是類,不是方法,也不是框架。僅僅是一個函數(shù)?!?~ John Carmack

從最簡單的實現(xiàn)開始,根據(jù)需要去改變成更加復(fù)雜的實現(xiàn)方式。當(dāng)涉及到對象時,整個變化過程看起來就像這樣:

Pure function -> factory -> functional mixin -> class
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

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

友情鏈接更多精彩內(nèi)容