[譯] ES6+ 中的 JavaScript 工廠函數(第八部分)

Smoke Art Cubes to Smoke — MattysFlicks — (CC BY 2.0)
Smoke Art Cubes to Smoke — MattysFlicks — (CC BY 2.0)

注意:這是「軟件編寫」系列文章的第八部分,該系列主要闡述如何在 JavaScript ES6+ 中從零開始學習函數式編程和組合化軟件(compositional software)技術(譯註:關於軟件可組合性的概念,參見維基百科 Composability)。後續還有更多精彩內容,敬請期待!
< 上一篇 | << 第一篇 | 下一篇 >javascript

工廠函數是一個能返回對象的函數,它既不是類也不是構造函數。在 JavaScript 中,任何函數均可以返回一個對象,若是函數前面沒有使用 new 關鍵字,卻又返回一個對象,那這個函數就是一個工廠函數。前端

由於工廠函數提供了輕鬆生成對象實例的能力,且無需深刻學習類和 new 關鍵字的複雜性,因此工廠函數在 JavaScript 中一直很具吸引力。java

JavaScript 提供了很是方便的對象字面量語法,代碼以下:react

const user = {
  userName: 'echo',
  avatar: 'echo.png'
};複製代碼

就像 JSON 的語法(JSON 就是基於 JavaScript 的對象字面量語法),:(冒號)左邊是屬性名,右邊是屬性值。你可使用點運算符訪問變量:android

console.log(user.userName); // "echo"複製代碼

或者使用方括號及屬性名訪問變量:ios

const key = 'avatar';
console.log( user[key] ); // "echo.png"複製代碼

若是在做用域內還有變量和你的屬性名相同,那你能夠直接在對象字面量中使用這個變量,這樣就省去了冒號和屬性值:git

const userName = 'echo';
const avatar = 'echo.png';
const user = {
  userName,
  avatar
};
console.log(user);
// { "avatar": "echo.png",   "userName": "echo" }複製代碼

對象字面量支持簡潔表示法。咱們能夠添加一個 .setUserName() 的方法:es6

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 指向的是調用該方法的對象,要調用一個對象的方法,只須要簡單地使用點運算符訪問方法並使用圓括號調用便可,例如 game.play() 就是在 game 這一對象上調用 .play()。要使用點運算符調用方法,這個方法必須是對象屬性。你也可使用函數原型方法 .call().apply().bind() 把一個方法應用於一個對象上。github

本例中,user.setUserName('Foo') 是在 user 對象上調用 .setUserName(),所以 this === user。在.setUserName() 方法中,咱們經過 this 這個引用修改了 .userName 的值,而後返回了相同的對象實例,以便於後續方法鏈式調用。typescript

字面量偏向單一對象,工廠方法適用衆多對象

若是你須要建立多個對象,你應該考慮把對象字面量和工廠函數結合使用。

使用工廠函數,你能夠根據須要建立任意數量的用戶對象。假如你正在開發一個聊天應用,你會用一個用戶對象表示當前用戶,以及用不少個用戶對象表示其餘已登陸和在聊天的用戶,以便顯示他們的名字和頭像等等。

讓咱們把 user 對象轉換爲一個 createUser() 工廠方法:

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]
}
*/複製代碼

返回對象

箭頭函數(=>)具備隱式返回的特性:若是函數體由單個表達式組成,則能夠省略 return 關鍵字。()=>'foo' 是一個沒有參數的函數,並返回字符串 "foo"

返回對象字面量時要當心。當使用大括號時,JavaScript 默認你建立的是一個函數體,例如 { broken: true }。若是你須要返回一個明確的對象字面量,那你就須要經過使用圓括號將對象字面量包起來以消除歧義,以下所示:

const noop = () => { foo: 'bar' };
console.log(noop()); // undefined
const createFoo = () => ({ foo: 'bar' });
console.log(createFoo()); // { foo: "bar" }複製代碼

在第一個例子中,foo: 被解釋爲一個標籤,bar 被解釋爲一個沒有被賦值或者返回的表達式,所以函數返回 undefined

createFoo() 例子中,圓括號強制着大括號,使其被解釋爲要求值的表達式,而不是一個函數體。

解構

請特別注意函數聲明:

const createUser = ({ userName, avatar }) => ({複製代碼

這一行裏,大括號 ({, }) 表示對象解構。這個函數有一個參數(即一個對象),可是從這個參數中,卻解構出了兩個形參,userNameavatar。這些形參能夠做爲函數體內的變量使用。解構還能夠用於數組:

const swap = ([first, second]) => [second, first];
console.log( swap([1, 2]) ); // [2, 1]複製代碼

你可使用擴展語法 (...varName) 獲取數組(或參數列表)餘下的值,而後將這些值回傳成單個元素:

const rotate = ([first, ...rest]) => [...rest, first];
console.log( rotate([1, 2, 3]) ); // [2, 3, 1]複製代碼

計算屬性值

前面咱們使用方括號的方法動態訪問對象的屬性值:

const key = 'avatar';
console.log( user[key] ); // "echo.png"複製代碼

咱們也能夠計算屬性值來賦值:

const arrToObj = ([key, value]) => ({ [key]: value });
console.log( arrToObj([ 'foo', 'bar' ]) ); // { "foo": "bar" }複製代碼

本例中,arrToObj 接受一個包含鍵值對(又稱元組)的數組,並將其轉化成一個對象。由於咱們並不知道屬性名,所以咱們須要計算屬性名以便在對象上設置屬性值。爲了作到這一點,咱們使用了方括號表示法,來設置屬性名,並將其放在對象字面量的上下文中來建立對象:

{ [key]: value }複製代碼

在賦值完成後,咱們就能獲得像下面這樣的對象:

{ "foo": "bar" }複製代碼

默認參數

JavaScript 函數支持默認參數值,給咱們帶來如下優點:

  1. 用戶能夠經過適當的默認值省略參數。
  2. 函數自我描述性更高,由於默認值提供預期的輸入例子。
  3. IDE 和靜態分析工具能夠利用默認值推斷參數的類型。例如,一個默認值 1 表示參數能夠接受的數據類型爲 Number

使用默認參數,咱們能夠爲咱們的 createUser 工廠函數描述預期的接口,此外,若是用戶沒有提供信息,能夠自動地補充某些細節:

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()
);複製代碼

函數簽名的最後一部分可能看起來有點搞笑:

} = {}) => ({複製代碼

在參數聲明最後那部分的 = {} 表示:若是傳進來的實參不符合要求,則將使用一個空對象做爲默認參數。當你嘗試從空對象解構賦值的時候,屬性的默認值會被自動填充,由於這就是默認值所作的工做:用預先定義好的值替換 undefined

若是沒有 = {} 這個默認值,且沒有向 createUser() 傳遞有效的實參,則將會拋出錯誤,由於你不能從 undefined 中訪問屬性。

類型判斷

在寫這篇文章的時候,JavaScript 都尚未內置的類型註解,可是近幾年涌現了一批格式化工具或者框架來填補這一空白,包括 JSDoc(因爲出現了更好的選擇其呈現出降低趨勢)、Facebook 的 Flow、還有微軟的 TypeScript。我我的使用 rtype,由於我以爲它在函數式編程方面比 TypeScript 可讀性更強。

直至寫這篇文章,各類類型註解方案其實都不相上下。沒有一個得到 JavaScript 規範的庇護,並且每一個方案都有它明顯的不足。

類型推斷是基於變量所在的上下文推斷其類型的一個過程,在 JavaScript 中,這是對類型註解很是好的一個替代。

若是你在標準的 JavaScript 函數中提供足夠的線索去推斷,你就能得到類型註解的大部分好處,且不用擔憂任何額外成本或風險。

即便你決定使用像 TypeScript 或 Flow 這樣的工具,你也應該儘量利用類型推斷的好處,並保存在類型推斷抽風時的類型註解。例如,原生 JavaScript 是不支持定義共享接口的。但使用 TypeScript 或 rtype 均可以方便有效地定義接口。

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

微軟的 Visual Studio Code 不須要 Tern,由於它把 TypeScript 的類型推斷功能附帶到了 JavaScript 代碼的編寫中。

當你在 JavaScript 函數中指定默認參數值時,不少諸如 Tern.js、TypeScript 和 Flow 的類型推斷工具就能夠在 IDE 中給予提示以幫助開發者正確地使用 API。

沒有默認值,各類 IDE(更多的時候,連咱們本身)都沒有足夠的信息來判斷函數預期的參數類型。

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

有了默認值,IDE (更多的時候,咱們本身) 能夠從代碼中推斷出類型。

有默認值,IDE 能夠提示 `userName` 的類型應該是字符串。
有默認值,IDE 能夠提示 `userName` 的類型應該是字符串。

將參數限制爲固定類型(這會使通用函數和高階函數更加受限)是不怎麼合理的。但要說這種方法何時有意義的話,使用默認參數一般就是,即便你已經在使用 TypeScript 或 Flow 作類型推斷。

Mixin 結構的工廠函數

工廠函數擅於利用一個優秀的 API 建立對象。一般來講,它們能知足基本需求,但不久以後,你就會遇到這樣的狀況,總會把相似的功能構建到不一樣類型的對象中,因此你須要把這些功能抽象爲 mixin 函數,以便輕鬆重用。

mixin 的工廠函數就要大顯身手了。咱們來構建一個 withConstructor 的 mixin 函數,把 .constructor 屬性添加到全部的對象實例中。

with-constructor.js:

const withConstructor = constructor => o => {
  const proto = Object.assign({},
    Object.getPrototypeOf(o),
    { constructor }
  );
  return Object.assign(Object.create(proto), o);
};複製代碼

如今你能夠導入和使用其餘 mixins:

import withConstructor from `./with-constructor'; const pipe = (...fns) => x => fns.reduce((y, f) => f(y), x); // 或者 `import pipe from 'lodash/fp/flow';` // 設置一些 mixin 的功能 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() 能夠被用來模型飛行汽車、火箭或氣球。

對象組合更多的是一種思惟方式,而不是寫代碼的某一特定技巧。你能夠在不少地方用到它。功能組合只是從頭開始構建你思惟方式的最簡單方法,工廠函數就是將對象組合有關實現細節包裝成一個友好 API 的簡單方法。

結論

對於對象的建立和工廠函數,ES6 提供了一種方便的語法,大多數時候,這樣就足夠了,但由於這是 JavaScript,因此還有一種更方便並更像 Java 的語法:class 關鍵字。

在 JavaScript 中,類比工廠更冗長和受限,當進行代碼重構時更容易出現問題,但也被像是 React 和 Angular 等主流前端框架所採納使用,並且還有一些少見的用例,使得類更有存在乎義。

「有時,最優雅的實現僅僅是一個函數。不是方法,不是類,不是框架。僅僅只是一個函數。」 ~ John Carmack

最後,你還要切記,不要把事情搞複雜,工廠函數不是必需的,對於某個問題,你的解決思路應當是:

純函數 > 工廠函數 > 函數式 Mixin > 類

Next: Why Composition is Harder with Classes >

接下來

想更深刻學習關於 JavaScript 的對象組合?

跟着 Eric Elliott 學 Javacript,機不可失時再也不來!

Eric Elliott「編寫 JavaScript 應用」 (O’Reilly) 以及 「跟着 Eric Elliott 學 Javascript」 兩書的做者。他爲許多公司和組織做過貢獻,例如 Adobe SystemsZumba FitnessThe Wall Street JournalESPNBBC 等 , 也是不少機構的頂級藝術家,包括但不限於 UsherFrank Ocean 以及 Metallica

大多數時間,他都在 San Francisco Bay Area,同這世上最美麗的女子在一塊兒。

感謝 JS_Cheerleader.


掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 AndroidiOSReact前端後端產品設計 等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章
相關標籤/搜索