- 原文地址:A practical guide to writing more functional JavaScript
- 原文做者:Nadeesha Cabral
- 本文永久連接:github-heyushuo-blob
- 譯者:heyushuo
一切皆爲函數javascript
函數式編程很棒。隨着 React 的引入,愈來愈多的 JavaScript 前端代碼正在考慮 FP 原則。可是咱們如何在咱們編寫的平常代碼中開始使用 FP 思惟模式?我將嘗試使用平常代碼塊並逐步重構它。前端
咱們的問題:用戶來到咱們的登陸頁面連接後會帶一個redirect_to
參數。就像/login?redirect_to =%2Fmy-page
。請注意,當%2Fmy-page
被編碼爲 URL
的一部分時,它其實是/ my-page
。咱們須要提取此參數,並將其存儲在本地存儲中,以便在完成登陸後,能夠將用戶重定向到 my-page
頁面。java
若是咱們以最簡單方式來呈現這個解決方案,咱們將如何編寫它?咱們須要以下幾個步驟git
咱們還必須將不安全
的函數放到try catch
塊中。有了這些,咱們的代碼將以下所示:github
function persistRedirectToParam() {
let parsedQueryParam;
try {
//獲取鏈接後的參數{redirect_to:'/my-page'}
parsedQueryParam = qs.parse(window.location.search); // https://www.npmjs.com/package/qs
} catch (e) {
console.log(e);
return null;
}
//獲取到參數
const redirectToParam = parsedQueryParam.redirect_to;
if (redirectToParam) {
const decodedPath = decodeURIComponent(redirectToParam);
try {
localStorage.setItem('REDIRECT_TO', decodedPath);
} catch (e) {
console.log(e);
return null;
}
//返回 my-page
return decodedPath;
}
return null;
}
複製代碼
暫時,讓咱們忘記 try catch
塊並嘗試將全部內容表達爲函數。npm
// // 讓咱們聲明全部咱們須要的函數
const parseQueryParams = query => qs.parse(query);
const getRedirectToParam = parsedQuery => parsedQuery.redirect_to;
const decodeString = string => decodeURIComponent(string);
const storeRedirectToQuery = redirectTo => localStorage.setItem('REDIRECT_TO', redirectTo);
function persistRedirectToParam() {
//使用它們
const parsed = parseQueryParams(window.location.search);
const redirectTo = getRedirectToParam(parsed);
const decoded = decodeString(redirectTo);
storeRedirectToQuery(decoded);
return decoded;
}
複製代碼
當咱們開始將全部「結果」用函數的方式表示時,咱們會看到咱們能夠從主函數體中重構的內容。這樣處理後,咱們的函數變得更容易理解,而且更容易測試。編程
早些時候,咱們將測試主要函數做爲一個總體。可是如今,咱們有 4 個較小的函數,其中一些只是代理其餘函數,所以須要測試的足跡要小得多。數組
讓咱們識別這些代理函數,並刪除代理,這樣咱們就能夠減小一些代碼。安全
const getRedirectToParam = parsedQuery => parsedQuery.redirect_to;
const storeRedirectToQuery = redirectTo => localStorage.setItem('REDIRECT_TO', redirectTo);
function persistRedirectToParam() {
const parsed = qs.parse(window.location.search);
const redirectTo = getRedirectToParam(parsed);
const decoded = decodeURIComponent(redirectTo);
storeRedirectToQuery(decoded);
return decoded;
}
複製代碼
好的。如今,彷佛 persistRedirectToParam
函數是 4 個其餘函數的「組合」讓咱們看看咱們是否能夠將此函數編寫爲合成,從而消除咱們存儲爲 const
的中間結果。bash
const getRedirectToParam = (parsedQuery) => parsedQuery.redirect_to;
// we have to re-write this a bit to return a result.
const storeRedirectToQuery = (redirectTo) => {
localStorage.setItem("REDIRECT_TO", redirectTo)
return redirectTo;
};
function persistRedirectToParam() {
const decoded = storeRedirectToQuery(
decodeURIComponent(
getRedirectToParam(
qs.parse(window.location.search)
)
)
)
return decoded;
}
複製代碼
這很好。可是我同情讀取這個嵌套函數調用的人。若是有辦法解開這個混亂,那就太棒了。
若是你已經完成了以上的一些重構,那麼你就會遇到compose
。Compose
是一個實用函數,它接受多個函數,並返回一個逐個調用底層函數的函數。還有其餘很好的資源來學習 composition
,因此我不會在這裏詳細介紹。
使用 compose
,咱們的代碼將以下所示:
const compose = require('lodash/fp/compose');
const qs = require('qs');
const getRedirectToParam = parsedQuery => parsedQuery.redirect_to;
const storeRedirectToQuery = redirectTo => {
localStorage.setItem('REDIRECT_TO', redirectTo);
return redirectTo;
};
function persistRedirectToParam() {
const op = compose(
storeRedirectToQuery,
decodeURIComponent,
getRedirectToParam,
qs.parse
);
return op(window.location.search);
}
複製代碼
compose 內的函數執行順序爲從右向左,即最右邊的函數(最後一個參數)最早執行,執行完的結果做爲參數傳遞給前一個函數。所以,在 compose 鏈中調用的第一個函數是最後一個函數。
若是你是一名數學家而且熟悉這個概念,這對你來講不是一個問題,因此你天然會從右到左閱讀。但對於熟悉命令式代碼的其餘人來講,咱們想從左到右閱讀。
幸運的是這裏有pipe(管道)
和 compose
作了一樣的事情,可是執行順序和 compose
是相反的,所以鏈中的第一個函數最早執行,執行完的結果做爲參數傳遞給下一個函數。
並且,彷佛咱們的 persistRedirectToParams
函數已經成爲另外一個咱們稱之爲 op
的函數的包裝器。換句話說,它所作的只是執行op
。咱們能夠擺脫包裝並「扁平化」咱們的函數。
const pipe = require('lodash/fp/pipe');
const qs = require('qs');
const getRedirectToParam = parsedQuery => parsedQuery.redirect_to;
const storeRedirectToQuery = redirectTo => {
localStorage.setItem('REDIRECT_TO', redirectTo);
return redirectTo;
};
const persistRedirectToParam = fp.pipe(
qs.parse,
getRedirectToParam,
decodeURIComponent,
storeRedirectToQuery
);
複製代碼
差很少了。請記住,咱們適當地將 try-catch 塊留在後面,以使其達到正確的狀態?好的接下來,咱們須要一些方式來介紹它。qs.parse 和 storeRedirectToQuery 都是不安全。一種選擇是使它們成爲包裝函數並將它們放在 try-catch 塊中。另外一種函數式方式
是將 try-catch
表示爲一種函數。
有一些實用程序作到了這一點,但讓咱們本身嘗試寫一些東西。
function tryCatch(opts) {
return args => {
try {
return opts.tryer(args);
} catch (e) {
return opts.catcher(args, e);
}
};
}
複製代碼
咱們的函數在這裏須要一個包含 tryer 和 catcher 函數的 opts 對象。它將返回一個函數,當使用參數調用時,使用所述參數調用 tryer 並在失敗時調用 catcher。如今,當咱們有不安全的操做時,咱們能夠將它們放入 tryer 部分,若是它們失敗,則從捕獲器部分進行救援並提供安全結果(甚至記錄錯誤)。
所以,考慮到這一點,咱們的最終代碼以下:
const pipe = require('lodash/fp/pipe');
const qs = require('qs');
const getRedirectToParam = parsedQuery => parsedQuery.redirect_to;
const storeRedirectToQuery = redirectTo => {
localStorage.setItem('REDIRECT_TO', redirectTo);
return redirectTo;
};
const persistRedirectToParam = fp.pipe(
tryCatch({
tryer: qs.parse,
catcher: () => {
return {
redirect_to: null // we should always give back a consistent result to the subsequent function
};
}
}),
getRedirectToParam,
decodeURIComponent,
tryCatch({
tryer: storeRedirectToQuery,
catcher: () => null // if localstorage fails, we get null back
})
);
// to invoke, persistRedirectToParam(window.location.search);
複製代碼
這或多或少是咱們想要的。可是爲了確保代碼的可讀性和可測試性獲得改善,咱們也能夠將「安全」函數(tryCatch 函數)分解出來。
const pipe = require('lodash/fp/pipe');
const qs = require('qs');
const getRedirectToParam = parsedQuery => parsedQuery.redirect_to;
const storeRedirectToQuery = redirectTo => {
localStorage.setItem('REDIRECT_TO', redirectTo);
return redirectTo;
};
const safeParse = tryCatch({
tryer: qs.parse,
catcher: () => {
return {
redirect_to: null // we should always give back a consistent result to the subsequent function
};
}
});
const safeStore = tryCatch({
tryer: storeRedirectToQuery,
catcher: () => null // if localstorage fails, we get null back
});
const persistRedirectToParam = fp.pipe(
safeParse,
getRedirectToParam,
decodeURIComponent,
safeStore
);
複製代碼
如今,咱們獲得的是一個更強大功能的函數,由 4 個獨立的函數組成,這些函數具備高度內聚性,鬆散耦合,能夠獨立測試,能夠獨立重用,考慮異常場景,而且具備高度聲明性。
有一些 FP 語法糖使這變得更好,可是這是之後的某一天。
若是發現譯文存在錯誤或其餘須要改進的地方請指出。