01 March 2018
号外号外:我的新书《现代JavaScript库开发:原理、技术与实战》出版啦!!!快点我查看
号外号外:一组小而美的JavaScript迷你库!!!快点我查看
号外号外:猿辅导招聘前端,后端,客户端啦!地点:北京!!!快点我查看

函数式编程太火了,我也想学!!!

函数式编程太难了,该怎么入门???

嗯,函数式编程这么火,是一定要学的,但是怎么学呢?本文通过一个例子来介绍函数式编程的用法,先声明本文没有复杂的概念,也没有各种定义,只有一个由浅入深的例子

背景

假设有一个如下的数据结构,type 是 1-n,flag 表示当前元素的状态,页面大概是一个列表,可以通过类型和选择状态来过滤列表内容

const list = [
  {
    type: 1,
    flag: true,
  },
  {
    type: 2,
    flag: false,
  },
  {
    type: 3,
    flag: true,
  },
];

可能的需求有下面这些:

  • 获取全部的列表
  • 获取一个或多个类型的列表
  • 获取否定类型的列表
  • 获取指定状态的列表
  • 获取指定状态,指定类型的列表

呃呃呃,有点懵是不是,思考下这个程序该怎么写?如果写一个函数满足上面所有的需求呢?

获取全部的列表

下面先来写第一个需求,需要用到数组的 filter 和函数的默认值,就是这么简单

function getList(
  filter = function () {
    return true;
  }
) {
  return list.filter(filter);
}

调用getList就能获取全部的列表,但上面的参数默认值其实可以提取出一个公共函数

// 高阶函数
function bool(flag) {
  return function () {
    return !!flag;
  };
}

bool(true); // function { return true; }
bool(false); // function { return false; }

快来用 bool 来重写上面的代码吧,更简洁了是不是

function getList(filter = bool(true)) {
  return list.filter(filter);
}

抽象出来的 bool 是一个高阶函数,bool 让返回布尔值的函数变得可以复用,高阶函数让函数可以被复用

获取一个或多个类型的列表

如果想获取指定类型的列表,最简单的写法如下

function getList(filter = bool(true)) {
  return list.filter(filter);
}

getList(function (data) {
  return data.type === 1;
});
getList(function (data) {
  return data.type === 2;
});
getList(function (data) {
  return data.type === 3;
});

上面代码中多次调用data.type,我们把获取对象属性的功能抽象出来

function pick(obj, prop) {
  return obj[prop];
}

pick(data, 'type'); // data.type

下面用 pick 函数改写上面的代码

getList(function (data) {
  return pick(data, 'type') === 1;
});
getList(function (data) {
  return pick(data, 'type') === 2;
});
getList(function (data) {
  return pick(data, 'type') === 3;
});

好像更复杂了。。。要是能把 type 参数提前绑定呢?绑定函数参数好像是函数柯里化,下面的 currying 函数

function currying(func, ...args) {
  return func.bind(null, ...args);
}

function add(x, y) {
  return x + y;
}

const add10 = currying(add, 10);

add10(1); // 11
add10(5); // 15

但 currying 的绑定顺序是从左到右的,上面我们希望的先绑定第二个参数,如果能反转一下参数顺序就好了

function reverseArgs(...args) {
  return args.reverse();
}

reverseArgs(1, 2, 3); // [3, 2, 1]

怎么才能把 reverseArgs 和我们的函数结合起来呢?下面介绍一个神奇的高阶函数

// 返回一个函数A,A在被执行时会依次执行传入的函数参数
// compose(fn1, fn2, fn3)(1) 相当于fn3(...fn2(...fn1(1)))
function compose(...fns) {
  return function (...args) {
    return fns.reduce((prev, fn) => fn(...[].concat(prev)), args);
  };
}

有了 currying 和反转参数的代码再来改造下我们上面的代码,豁然开朗有木有

const reversePick = compose(reverseArgs, pick); // 参数顺序被反转 reversePick('type', data)
const pickType = currying(reversePick, 'type'); // 先绑定第一个参数 pickType(data)

getList(function (data) {
  return pickType(data) === 1;
});
getList(function (data) {
  return pickType(data) === 2;
});
getList(function (data) {
  return pickType(data) === 3;
});

但上面的代码并不能执行,由于数组 filter 函数,传给每个 filter 的参数并不是一个,而是三个,这就影响到了我们的 reverseArgs,操作,随意需要先进性裁切操作,还需要引入一个函数

function sliceArgs(num, ...args) {
  return args.slice(0, num);
}

const sliceArgs2 = currying(sliceArgs, 2);

const reversePick = compose(sliceArgs2, reverseArgs, pick);

上面的代码中的另一个问题是存在多次判断的代码,下面提取出来

function isEqual(x, y) {
  return x === y;
}

const isType1 = currying(isEqual, 1);
const isType2 = currying(isEqual, 2);
const isType3 = currying(isEqual, 3);

getList(function (data) {
  return isType1(pickType(data));
});
getList(function (data) {
  return isType2(pickType(data));
});
getList(function (data) {
  return isType3(pickType(data));
});

显然代码好像更复杂了,别急继续往下看,上面代码重复写了三个函数,利用前面的compose可以把这个函数也消除,是不是很神奇

// compose(pickType, isType1(data) = function () { isType1(pickType(data)) }
getList(compose(pickType, isType1));
getList(compose(pickType, isType2));
getList(compose(pickType, isType3));

上面解决了一个类型的判断,如果相判断 1 或 2 该怎么做呢?还需要一个或操作的高阶函数

function or(...fns) {
    return function (...agrs) {
        return fns.some(fn => fn(...args))
    }
}

getList(compose(pickType, or(isType1, isType2))

获取否定类型的列表

下面来看看如何获取否定类型的列表,比如获取所有 type 非 1 的,看起来需要一个取非的函数

function not(fn) {
    return function (...args) {
        return !fn(...args)
    }
}

getList(compose(pickType, not(isType1))

获取指定状态的列表

下面来看看如何获取指定类型的列表,其实参考上面的过程就很容易得出

const isFlagTrue = currying(isEqual, true);
const isFlagFalse = currying(isEqual, false);

const pickFlag = currying(compose(sliceArgs2, reverseArgs, pick), 'flag'); // currying reverseArgs 见上面

getList(compose(pickFlag, isFlagTrue)); // 获取flag为true的列表
getList(compose(pickFlag, isFlagFalse)); // 获取flag为false的列表

获取指定状态,指定类型的列表

有了获取指定状态和指定类型的代码,那么如何指定两个呢?我们需要一个并操作的高阶函数

function and(...fns) {
  return function (...agrs) {
    return fns.every((fn) => fn(...args));
  };
}

// 获取类型为1,flag为true的列表
getList(and(compose(pickType, isType1), compose(pickFlag, isFlagTrue)));

// 获取类型不为1,flag为true的列表
getList(and(compose(pickType, not(isType1)), compose(pickFlag, isFlagTrue)));

// 获取类型为1或2,并且flag为true的列表
getList(
  and(compose(pickType, or(isType1, isType2)), compose(pickFlag, isFlagTrue))
);

总结

整个过程我们抽象了很多通用函数,这些函数都是可以复用的

function bool(flag) {
  return function () {
    return !!flag;
  };
}

function pick(obj, prop) {
  return obj[prop];
}

function currying(func, ...args) {
  return func.bind(null, ...args);
}

function reverseArgs(...args) {
  return args.reverse();
}

function sliceArgs(num, ...args) {
  return args.slice(0, num);
}

function compose(...fns) {
  return function (...args) {
    return fns.reduce((prev, fn) => fn(...[].concat(prev)), args);
  };
}

function isEqual(x, y) {
  return x === y;
}

function or(...fns) {
  return function (...args) {
    return fns.some((fn) => fn(...args));
  };
}

function not(fn) {
  return function (...args) {
    return !fn(...args);
  };
}

function and(...fns) {
  return function (...args) {
    return fns.every((fn) => fn(...args));
  };
}

完整的业务代码如下,有了这些函数,代码变得非常简单,易读,并且非常灵活,可以随意组合

const sliceArgs2 = currying(sliceArgs, 2);

const pickType = currying(compose(sliceArgs2, reverseArgs, pick), 'type');
const isType1 = currying(isEqual, 1);
const isType2 = currying(isEqual, 2);
const isType3 = currying(isEqual, 3);

const pickFlag = currying(compose(sliceArgs2, reverseArgs, pick), 'flag');
const isFlagTrue = currying(isEqual, true);
const isFlagFalse = currying(isEqual, false);

const list = [
  {
    type: 1,
    flag: true,
  },
  {
    type: 2,
    flag: false,
  },
  {
    type: 3,
    flag: true,
  },
];

function getList(filter = bool(true)) {
  return list.filter(filter);
}

// 获取类型为1,flag为true的列表
getList(and(compose(pickType, isType1), compose(pickFlag, isFlagTrue)));

// 获取类型不为1,flag为true的列表
getList(and(compose(pickType, not(isType1)), compose(pickFlag, isFlagTrue)));

// 获取类型为1或2,并且flag为true的列表
getList(
  and(compose(pickType, or(isType1, isType2)), compose(pickFlag, isFlagTrue))
);

下面给出对应的非函数式写法的代码,对比一下,大量的过程式代码,区别还是很大的,如果是你,你会使用那种方式呢?

function getList(filter = bool(true)) {
  return list.filter(filter);
}

// 获取类型为1,flag为true的列表
getList(function (data) {
  return data.type === 1 && data.flag === true;
});

// 获取类型不为1,flag为true的列表
getList(function (data) {
  return data.type !== 1 && data.flag === true;
});

// 获取类型为1或2,并且flag为true的列表
getList(function (data) {
  return (data.type === 1 || data.type === 2) && data.flag === true;
});

最后给大家推荐一本 javascript 函数式编程的书籍《JavaScript 函数式编程》,如果对函数式感兴趣的话就赶紧入手吧

原文网址:http://yanhaijing.com/javascript/2018/03/01/functional-programming-practice/

微信公众号:颜海镜
关注微信公众号 颜海镜
微信支付二维码
赞赏支持 微信扫一扫