实用指南: 编写函数式 JavaScript
Posted freeCodeCamp
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了实用指南: 编写函数式 JavaScript相关的知识,希望对你有一定的参考价值。
原文:https://www.freecodecamp.org/news/a-practical-guide-to-writing-more-functional-javascript-db49409f71/
译者:zhicheng
校对者:lily
提示:文中的蓝色字体大家可以点击文末“阅读原文”在 freeCodeCamp 中文论坛访问链接
一切都是函数。
函数式编程很棒。随着 React 的推进,越来越多的 JavaScript 前端代码开始基于 FP 原则开发。怎样在日常的编码中使用 FP 呢?让我们来一起写一些日常代码,然后一步步重构它。
场景: 用户来到 /login
页,可能带有 redirect_to
参数。比如 /login?redirect_to=%2Fmy-page
。注意 %2Fmy-page
其实是 /my-page
做为 URL 的部分编码后的样子。我们需要将请求参数取出来,然后存储到 local Storage 里,这样登录成功,用户就可以直接跳转到 my-page
页了。
第 0 步:迫切的方法
如果需要紧急上线一个解决方案,应该怎么写呢?需求如下:
解析请求参数。
得到
redirect_to
的值。解码。
存储到 localStorage。
对于可能会抛出异常的函数,还要用 try catch 语句嵌套起来。综上,代码如下:
function persistRedirectToParam() {
let parsedQueryParam;
try {
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;
}
return decodedPath;
}
return null;
}
第 1 步:每一步都改为函数
此刻,先忘掉 try catch 语句,把一切都改写成函数。
// let's declare all of the functions we need to have
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() {
// and let's call them
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;
}
第 2 步: 尝试组合函数
现在,presisRedirectToParams
看起来更像是其它 4 个函数的合集。看看能不能把它们写成一个,从而省去 const
定义的中间结果。
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;
}
看起来不错,但是函数多层嵌套看着有点别扭,如果能去掉就好了。
第 3 步: 可读性更强
如果用过 redux 或者 recompose,那么你应该知道 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);
}
需要注意的是 compse 从右向左执行函数,也就是说,第一个被 compose
链调用的反而是最后一个函数。
站在数学角度讲这不难理解,就像概念描述的那样,从右向左很自然。但对于重构代码的我们来说,更希望是从左向右的顺序。
第 4 步: Piping 以及 flattening
万幸,有 pipe
,pipe
和 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
)
// to invoke, persistRedirectToParam(window.location.search);
好事将近。别忘了,我们之前剥离了 try-catch
语句块,无视了一些风险,现在需要以某种方式再将其引入进来。qs.parse
和 storeRedirectToQuery
是不安全的,方法之一是把他们嵌套在一个 try-catch
语句块内,另一个更函数式的方法是将 try-catch
做为一个函数。
第 5 步:处理函数的异常
有很多库可以搞定它,但是现在需要自己动手。
function tryCatch(opts) {
return (args) => {
try {
return opts.tryer(args);
} catch (e) {
return opts.catcher(args, e);
}
};
}
函数的参数是一个 opt
对象,包含了 tryper
和 catcher
函数。当传参时调用 tryer
,异常时调用 catcher
,然后返回。现在,不安全的操作可以把它放在 tryer
部分里,失败时处理异常,在 catcher
里给出一个备用的结果 ( 即使是出错了)。
第 6 步:把所有的东西都放在一起
最终,代码如下:
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);
这就是最终想要的。但是为了确保可读性以及易于测试,还可以抽离出不抛异常的函数。
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,
)
// to invoke, persistRedirectToParam(window.location.search);
现在我们实现了一下更庞大的函数,同时也是四个高内聚、低耦合、可测试、可复用、高容错、声明式的函数(当然,也更易读)。
还可以使用一些 FP 语法糖来做到更优雅,改天再聊。
推荐阅读:
点击阅读原文访问 freeCodeCamp 中文论坛的更多内容
以上是关于实用指南: 编写函数式 JavaScript的主要内容,如果未能解决你的问题,请参考以下文章