需求:写一个函数,输入 ‘chen’,返回 ‘HELLO, CHEN’。
试一下
const toUpperCase = function (x) {return x.toUpperCase();};const hello = function (x) {return "HELLO, " + x;};const greet = function (x) {return hello(toUpperCase(x));};console.log(greet("chen")); // 'HELLO, KEVIN'
只需两步,首先小写转大写,然后拼接字符串。如果有更多操作,greet 函数里就需要更多的嵌套,类似于 fn3(fn2(fn1(x)))。
compose 函数
功能是这样的:可以接收任意个函数作为参数,返回一个函数,这个函数接收的参数作为最右边函数的参数,而最右边这个函数的返回值作为倒数第二个函数的参数 ….,依此类推。
compose(fn1, fn2, fn3, fn3 ....);
function compose() {const args = arguments;const start = args.length - 1;return function () {let i = start;let result = args[start].apply(this, arguments);while (i--) result = args[i].call(this, result);return result;};}
这个 compose 已经是支持多个函数了,然而这个有什么用呢?
先了解一个概念 pointfree。
pointfree
pointfree 指的是函数无须提及将要操作的数据是怎么样的,以一开始的例子来讲解。
// 需求:输入 'kevin',返回 'HELLO, KEVIN'// 非 pointfree,因为提到了数据 nameconst greet = function (name) {return ("hello" + name).toUpperCase();};// pointfreefunction compose() {const args = arguments;const start = args.length - 1;return function () {let i = start;let result = args[start].apply(this, arguments);while (i--) result = args[i].call(this, result);return result;};}// 先定义基本运算,然后封装起来复用const toUpperCase = function (x) {return x.toUpperCase();};const hello = function (x) {return "HELLO, " + x;};const greet = compose(hello, toUpperCase);greet("kevin");
再举一个复杂的例子,我们需要一个 curry 函数,在 《javascript 柯里化》有讲。
需求:输入 ‘kevin daisy kelly’,返回 ‘K.D.K’
// 非 pointfree 提及到 nameconst initials = function (name) {return name.split(" ").map(compose(toUpperCase, head)).join(".");};// pointfree// 先定义基本运算const split = curry(function (separator, str) {return str.split(separator);});const head = function (str) {return str.slice(0, 1);};const toUpperCase = function (str) {return str.toUpperCase();};const join = curry(function (separator, arr) {return arr.join(separator);});const map = curry(function (fn, arr) {return arr.map(fn);});const initials = compose(join("."), map(compose(toUpperCase, head)), split(" "));initials("kevin daisy kelly");
可以看出利用 curry 和函数组合(compose)非常容易实现 pointfree。
其实这些基本函数已经有一个库已经帮我们做了,这样就不用自己每次都定义一遍,这个库叫 ramda.js。
// ramda.jsconst initials = R.compose(R.join('.'), R.map(R.compose(R.toUpper, R.head)), R.split(' '));
pointfree 本质是:使用一些通用的函数,组合出各种复杂的运算。上层运算不要直接操作数据,而是通过底层函数去处理。既不使用所要处理的值,只合成运算过程。
好处:pointfree 模式能够帮助我们减少不必要的命名,让代码保持简洁和通用,更符合语义,更容易复用,测试变得更简答。
实战
假设从服务器获取这样的数据
const data = {result: "SUCCESS",tasks: [{id: 104, complete: false, priority: "high",dueDate: "2013-11-29", username: "Scott",title: "Do something", created: "9/22/2013"},{id: 105, complete: false, priority: "medium",dueDate: "2013-11-22", username: "Lena",title: "Do something else", created: "9/22/2013"},{id: 107, complete: true, priority: "high",dueDate: "2013-11-22", username: "Mike",title: "Fix the foo", created: "9/22/2013"},{id: 108, complete: false, priority: "low",dueDate: "2013-11-15", username: "Punam",title: "Adjust the bar", created: "9/25/2013"},{id: 110, complete: false, priority: "medium",dueDate: "2013-11-15", username: "Scott",title: "Rename everything", created: "10/2/2013"},{id: 112, complete: true, priority: "high",dueDate: "2013-11-27", username: "Lena",title: "Alter all quuxes", created: "10/5/2013"}]};
需求:写一个 getIncompleteTaskSummaries 函数,接收一个 userName 的参数,从服务器获取数据,然后筛选出这名用户的未完成的任务的 ids、priorities、titles、和 dueDate 数据,并按照日期升序排序。
以 Scott 为例,最终筛选出的数据为:
[{id: 110, title: "Rename everything",dueDate: "2013-11-15", priority: "medium"},{id: 104, title: "Do something",dueDate: "2013-11-29", priority: "high"}]
过程式编程方法
const fetchData = function () {return Promise.resolve(data);};const getIncompleteTaskSummaries = function (memberName) {return fetchData().then(function (data) {return data.tasks;}).then(function (tasks) {return tasks.filter(function (task) {return task.username === memberName;});}).then(function (tasks) {return tasks.filter(function (task) {return !task.complete;});}).then(function (tasks) {return tasks.map(function (task) {return {id: task.id,dueDate: task.dueDate,title: task.title,priority: task.priority};});}).then(function (tasks) {return tasks.sort(function (first, second) {const a = first.dueDate, b = second.dueDate;return a < b ? -1 : a > b ? 1 : 0;});}).then(function (tasks) {console.log(tasks);});};getIncompleteTaskSummaries("Scott");
pointfree 模式
const fetchData = function () {return Promise.resolve(data);};// 定义基本函数const prop = curry(function (name, obj) {return obj[name];});const propEq = curry(function (name, val, obj) {return obj[name] === val;});const filter = curry(function (fn, arr) {return arr.filter(fn);});const map = curry(function (fn, arr) {return arr.map(fn);});const pick = curry(function (args, obj) {const result = {};for (let i = 0; i < args.length; i++) {result[args[i]] = obj[args[i]];}return result;});const sortBy = curry(function (fn, arr) {return arr.sort(function (a, b) {a = fn(a),b = fn(b);return a < b ? -1 : a > b ? 1 : 0;});});const getIncompleteTaskSummaries = function (memberName) {return fetchData().then(prop("tasks")).then(filter(propEq("username", memberName))).then(filter(propEq("complete", false))).then(map(pick(["id", "dueDate", "title", "priority"]))).then(sortBy(prop("dueDate"))).then(console.log);};getIncompleteTaskSummaries("Scott");
ramda.js 版本
const fetchData = function () {return Promise.resolve(data);};const getIncompleteTaskSummaries = function (membername) {return fetchData().then(R.prop("tasks")).then(R.filter(R.propEq("username", membername))).then(R.filter(R.propEq("complete", false))).then(R.map(R.pick(["id", "dueDate", "title", "priority"]))).then(R.sortBy(R.prop("dueDate"))).then(console.log);};getIncompleteTaskSummaries("Scott");
使用 compose 的 ramda.js 版本
const fetchData = function () {return Promise.resolve(data);};const getIncompleteTaskSummaries = function (membername) {return fetchData().then(R.compose(console.log,R.sortBy(R.prop("dueDate")),R.map(R.pick(["id", "dueDate", "title", "priority"])),R.filter(R.propEq("complete", false)),R.filter(R.propEq("username", membername)),R.prop("tasks"),));};getIncompleteTaskSummaries("Scott");
参考:
[1] JavaScript专题之函数组合
[2] 完整版例子
