- 原文地址:JavaScript Factory Functions with ES6+
- 原文做者:Eric Elliott
- 譯文出自:掘金翻譯計劃
- 本文永久連接:github.com/xitu/gold-m…
- 譯者:lampui
- 校對者:IridescentMia、sunui
注意:這是「軟件編寫」系列文章的第八部分,該系列主要闡述如何在 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 }) => ({複製代碼
這一行裏,大括號 ({, }
) 表示對象解構。這個函數有一個參數(即一個對象),可是從這個參數中,卻解構出了兩個形參,userName
和 avatar
。這些形參能夠做爲函數體內的變量使用。解構還能夠用於數組:
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
表示參數能夠接受的數據類型爲 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(更多的時候,連咱們本身)都沒有足夠的信息來判斷函數預期的參數類型。
有了默認值,IDE (更多的時候,咱們本身) 能夠從代碼中推斷出類型。
將參數限制爲固定類型(這會使通用函數和高階函數更加受限)是不怎麼合理的。但要說這種方法何時有意義的話,使用默認參數一般就是,即便你已經在使用 TypeScript 或 Flow 作類型推斷。
工廠函數擅於利用一個優秀的 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 Systems、Zumba Fitness、The Wall Street Journal、ESPN 和 BBC 等 , 也是不少機構的頂級藝術家,包括但不限於 Usher、Frank Ocean 以及 Metallica。
大多數時間,他都在 San Francisco Bay Area,同這世上最美麗的女子在一塊兒。
感謝 JS_Cheerleader.
掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 Android、iOS、React、前端、後端、產品、設計 等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。