- 原文地址: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 语法糖使这变得更好,可是这是之后的某一天。
若是发现译文存在错误或其余须要改进的地方请指出。