需求:写一个函数,输入 ‘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,因为提到了数据 name
const greet = function (name) {
return ("hello" + name).toUpperCase();
};
// pointfree
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;
};
}
// 先定义基本运算,然后封装起来复用
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 提及到 name
const 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.js
const 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] 完整版例子