首页 | 新闻 | 新品 | 文库 | 方案 | 视频 | 下载 | 商城 | 开发板 | 数据中心 | 座谈新版 | 培训 | 工具 | 博客 | 论坛 | 百科 | GEC | 活动 | 主题月 | 电子展
返回列表 回复 发帖

我眼中的JavaScript函数式编程

我眼中的JavaScript函数式编程

JavaScript 函数式编程是一个存在了很久的话题,但似乎从 2016 年开始,它变得越来越火热。这可能是因为 ES6 语法对于函数式编程更为友好,也可能是因为诸如 RxJS (ReactiveX) 等函数式框架的流行。
看过许多关于函数式编程的讲解,但是其中大部分是停留在理论层面,还有一些是仅针对 Haskell 等纯函数式编程语言的。而本文旨在聊一聊我眼中的函数式编程在 JavaScript 中的具体实践,之所以是 “我眼中的” 即我所说的仅代表个人观点,可能和部分 严格概念 是有冲突的。
本文将略去一大堆形式化的概念介绍,重点展示在 JavaScript 中到底什么是函数式的代码、函数式代码与一般写法有什么区别、函数式的代码能给我们带来什么好处以及常见的一些函数式模型都有哪些。
我理解的函数式编程
我认为函数式编程可以理解为,以函数作为主要载体的编程方式,用函数去拆解、抽象一般的表达式
与命令式相比,这样做的好处在哪?主要有以下几点:
    语义更加清晰
    可复用性更高
    可维护性更好
    作用域局限,副作用少
基本的函数式编程
下面例子是一个具体的函数式体现
// 数组中每个单词,首字母大写
// 一般写法
const arr = ['apple', 'pen', 'apple-pen'];
for(const i in arr){
  const c = arr[i][0];
  arr[i] = c.toUpperCase() + arr[i].slice(1);
}
console.log(arr);
// 函数式写法一
function upperFirst(word) {
  return word[0].toUpperCase() + word.slice(1);
}
function wordToUpperCase(arr) {
  return arr.map(upperFirst);
}
console.log(wordToUpperCase(['apple', 'pen', 'apple-pen']));
// 函数式写法二
console.log(arr.map(['apple', 'pen', 'apple-pen'], word => word[0].toUpperCase() + word.slice(1)));
当情况变得更加复杂时,表达式的写法会遇到几个问题:
    表意不明显,逐渐变得难以维护
    复用性差,会产生更多的代码量
    会产生很多中间变量
函数式编程很好的解决了上述问题。首先参看 函数式写法一,它利用了函数封装性将功能做拆解(粒度不唯一),并封装为不同的函数,而再利用组合的调用达到目的。这样做使得表意清晰,易于维护、复用以及扩展。其次利用 高阶函数,Array.map 代替 for…of 做数组遍历,减少了中间变量和操作。
而 函数式写法一 和 函数式写法二 之间的主要差别在于,可以考虑函数是否后续有复用的可能,如果没有,则后者更优。
链式优化
从上面 函数式写法二 中我们可以看出,函数式代码在写的过程中,很容易造成 横向延展,即产生多层嵌套,下面我们举个比较极端点的例子。
// 计算数字之和
// 一般写法
console.log(1 + 2 + 3 - 4)
// 函数式写法
function sum(a, b) {
  return a + b;
}
function sub(a, b) {
  return a - b;
}
console.log(sub(sum(sum(1, 2), 3), 4);
本例仅为展示 横向延展 的比较极端的情况,随着函数的嵌套层数不断增多,导致代码的可读性大幅下降,还很容易产生错误。
在这种情况下,我们可以考虑多种优化方式,比如下面的 链式优化 。
// 优化写法 (嗯,你没看错,这就是 lodash 的链式写法)
const utils = {
  chain(a) {
    this._temp = a;
    return this;
  },
  sum(b) {
    this._temp += b;
    return this;
  },
  sub(b) {
    this._temp -= b;
    return this;
  },
  value() {
    const _temp = this._temp;
    this._temp = undefined;
    return _temp;
  }
};
console.log(utils.chain(1).sum(2).sum(3).sub(4).value());
这样改写后,结构会整体变得比较清晰,而且链的每一环在做什么也可以很容易的展现出来。函数的嵌套和链式的对比还有一个很好的例子,那就是 回调函数 和 Promise 模式。
// 顺序请求两个接口
// 回调函数
import $ from 'jquery';
$.post('a/url/to/target', (rs) => {
  if(rs){
    $.post('a/url/to/another/target', (rs2) => {
      if(rs2){
        $.post('a/url/to/third/target');
      }
    });
  }
});
// Promise
import request from 'catta';  // catta 是一个轻量级请求工具,支持 fetch,jsonp,ajax,无依赖
request('a/url/to/target')
  .then(rs => rs ? $.post('a/url/to/another/target') : Promise.reject())
  .then(rs2 => rs2 ? $.post('a/url/to/third/target') : Promise.reject());
随着回调函数嵌套层级和单层复杂度增加,它将会变得臃肿且难以维护,而 Promise 的链式结构,在高复杂度时,仍能纵向扩展,而且层次隔离很清晰。
常见的函数式编程模型
闭包(Closure)
    可以保留局部变量不被释放的代码块,被称为一个闭包
闭包的概念比较抽象,相信大家都或多或少知道、用到这个特性
那么闭包到底能给我们带来什么好处?
先来看一下如何创建一个闭包:
// 创建一个闭包
function makeCounter() {
  let k = 0;
  return function() {
    return ++k;
  };
}
const counter = makeCounter();
console.log(counter());  // 1
console.log(counter());  // 2
继承事业,薪火相传
返回列表