实用指南: 编写函数式 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 步:迫切的方法

如果需要紧急上线一个解决方案,应该怎么写呢?需求如下:

  1. 解析请求参数。

  2. 得到 redirect_to 的值。

  3. 解码。

  4. 存储到 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

万幸,有 pipepipe 和 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_tonull// 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_tonull// 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的主要内容,如果未能解决你的问题,请参考以下文章

今日好书丨《前端函数式攻城指南》

前端函数式攻城指南 pdf

图书前端函数式攻城指南

javascript制作图javascript制作图片无限懒加载,轻松又实用片无限懒加载,轻松又实用

MSIL实用指南-闭包的生成和调用

《JavaScript面向对象编程指南》读书笔记—Function