需求:写一个函数,输入 ‘chen’,返回 ‘HELLO, CHEN’。

试一下

  1. const toUpperCase = function (x) {
  2. return x.toUpperCase();
  3. };
  4. const hello = function (x) {
  5. return "HELLO, " + x;
  6. };
  7. const greet = function (x) {
  8. return hello(toUpperCase(x));
  9. };
  10. console.log(greet("chen")); // 'HELLO, KEVIN'

只需两步,首先小写转大写,然后拼接字符串。如果有更多操作,greet 函数里就需要更多的嵌套,类似于 fn3(fn2(fn1(x)))。

compose 函数

功能是这样的:可以接收任意个函数作为参数,返回一个函数,这个函数接收的参数作为最右边函数的参数,而最右边这个函数的返回值作为倒数第二个函数的参数 ….,依此类推。

  1. compose(fn1, fn2, fn3, fn3 ....);
  1. function compose() {
  2. const args = arguments;
  3. const start = args.length - 1;
  4. return function () {
  5. let i = start;
  6. let result = args[start].apply(this, arguments);
  7. while (i--) result = args[i].call(this, result);
  8. return result;
  9. };
  10. }

这个 compose 已经是支持多个函数了,然而这个有什么用呢?

先了解一个概念 pointfree。

pointfree

pointfree 指的是函数无须提及将要操作的数据是怎么样的,以一开始的例子来讲解。

  1. // 需求:输入 'kevin',返回 'HELLO, KEVIN'
  2. // 非 pointfree,因为提到了数据 name
  3. const greet = function (name) {
  4. return ("hello" + name).toUpperCase();
  5. };
  6. // pointfree
  7. function compose() {
  8. const args = arguments;
  9. const start = args.length - 1;
  10. return function () {
  11. let i = start;
  12. let result = args[start].apply(this, arguments);
  13. while (i--) result = args[i].call(this, result);
  14. return result;
  15. };
  16. }
  17. // 先定义基本运算,然后封装起来复用
  18. const toUpperCase = function (x) {
  19. return x.toUpperCase();
  20. };
  21. const hello = function (x) {
  22. return "HELLO, " + x;
  23. };
  24. const greet = compose(hello, toUpperCase);
  25. greet("kevin");

再举一个复杂的例子,我们需要一个 curry 函数,在 《javascript 柯里化》有讲。

需求:输入 ‘kevin daisy kelly’,返回 ‘K.D.K’

  1. // 非 pointfree 提及到 name
  2. const initials = function (name) {
  3. return name.split(" ").map(compose(toUpperCase, head)).join(".");
  4. };
  5. // pointfree
  6. // 先定义基本运算
  7. const split = curry(function (separator, str) {
  8. return str.split(separator);
  9. });
  10. const head = function (str) {
  11. return str.slice(0, 1);
  12. };
  13. const toUpperCase = function (str) {
  14. return str.toUpperCase();
  15. };
  16. const join = curry(function (separator, arr) {
  17. return arr.join(separator);
  18. });
  19. const map = curry(function (fn, arr) {
  20. return arr.map(fn);
  21. });
  22. const initials = compose(join("."), map(compose(toUpperCase, head)), split(" "));
  23. initials("kevin daisy kelly");

可以看出利用 curry 和函数组合(compose)非常容易实现 pointfree。

其实这些基本函数已经有一个库已经帮我们做了,这样就不用自己每次都定义一遍,这个库叫 ramda.js

  1. // ramda.js
  2. const initials = R.compose(R.join('.'), R.map(R.compose(R.toUpper, R.head)), R.split(' '));

pointfree 本质是:使用一些通用的函数,组合出各种复杂的运算。上层运算不要直接操作数据,而是通过底层函数去处理。既不使用所要处理的值,只合成运算过程。

好处:pointfree 模式能够帮助我们减少不必要的命名,让代码保持简洁和通用,更符合语义,更容易复用,测试变得更简答。

实战

假设从服务器获取这样的数据

  1. const data = {
  2. result: "SUCCESS",
  3. tasks: [
  4. {
  5. id: 104, complete: false, priority: "high",
  6. dueDate: "2013-11-29", username: "Scott",
  7. title: "Do something", created: "9/22/2013"
  8. },
  9. {
  10. id: 105, complete: false, priority: "medium",
  11. dueDate: "2013-11-22", username: "Lena",
  12. title: "Do something else", created: "9/22/2013"
  13. },
  14. {
  15. id: 107, complete: true, priority: "high",
  16. dueDate: "2013-11-22", username: "Mike",
  17. title: "Fix the foo", created: "9/22/2013"
  18. },
  19. {
  20. id: 108, complete: false, priority: "low",
  21. dueDate: "2013-11-15", username: "Punam",
  22. title: "Adjust the bar", created: "9/25/2013"
  23. },
  24. {
  25. id: 110, complete: false, priority: "medium",
  26. dueDate: "2013-11-15", username: "Scott",
  27. title: "Rename everything", created: "10/2/2013"
  28. },
  29. {
  30. id: 112, complete: true, priority: "high",
  31. dueDate: "2013-11-27", username: "Lena",
  32. title: "Alter all quuxes", created: "10/5/2013"
  33. }
  34. ]
  35. };

需求:写一个 getIncompleteTaskSummaries 函数,接收一个 userName 的参数,从服务器获取数据,然后筛选出这名用户的未完成的任务的 ids、priorities、titles、和 dueDate 数据,并按照日期升序排序。

以 Scott 为例,最终筛选出的数据为:

  1. [
  2. {id: 110, title: "Rename everything",
  3. dueDate: "2013-11-15", priority: "medium"},
  4. {id: 104, title: "Do something",
  5. dueDate: "2013-11-29", priority: "high"}
  6. ]

过程式编程方法

  1. const fetchData = function () {
  2. return Promise.resolve(data);
  3. };
  4. const getIncompleteTaskSummaries = function (memberName) {
  5. return fetchData()
  6. .then(function (data) {
  7. return data.tasks;
  8. })
  9. .then(function (tasks) {
  10. return tasks.filter(function (task) {
  11. return task.username === memberName;
  12. });
  13. })
  14. .then(function (tasks) {
  15. return tasks.filter(function (task) {
  16. return !task.complete;
  17. });
  18. })
  19. .then(function (tasks) {
  20. return tasks.map(function (task) {
  21. return {
  22. id: task.id,
  23. dueDate: task.dueDate,
  24. title: task.title,
  25. priority: task.priority
  26. };
  27. });
  28. })
  29. .then(function (tasks) {
  30. return tasks.sort(function (first, second) {
  31. const a = first.dueDate, b = second.dueDate;
  32. return a < b ? -1 : a > b ? 1 : 0;
  33. });
  34. })
  35. .then(function (tasks) {
  36. console.log(tasks);
  37. });
  38. };
  39. getIncompleteTaskSummaries("Scott");

pointfree 模式

  1. const fetchData = function () {
  2. return Promise.resolve(data);
  3. };
  4. // 定义基本函数
  5. const prop = curry(function (name, obj) {
  6. return obj[name];
  7. });
  8. const propEq = curry(function (name, val, obj) {
  9. return obj[name] === val;
  10. });
  11. const filter = curry(function (fn, arr) {
  12. return arr.filter(fn);
  13. });
  14. const map = curry(function (fn, arr) {
  15. return arr.map(fn);
  16. });
  17. const pick = curry(function (args, obj) {
  18. const result = {};
  19. for (let i = 0; i < args.length; i++) {
  20. result[args[i]] = obj[args[i]];
  21. }
  22. return result;
  23. });
  24. const sortBy = curry(function (fn, arr) {
  25. return arr.sort(function (a, b) {
  26. a = fn(a),
  27. b = fn(b);
  28. return a < b ? -1 : a > b ? 1 : 0;
  29. });
  30. });
  31. const getIncompleteTaskSummaries = function (memberName) {
  32. return fetchData()
  33. .then(prop("tasks"))
  34. .then(filter(propEq("username", memberName)))
  35. .then(filter(propEq("complete", false)))
  36. .then(map(pick(["id", "dueDate", "title", "priority"])))
  37. .then(sortBy(prop("dueDate")))
  38. .then(console.log);
  39. };
  40. getIncompleteTaskSummaries("Scott");

ramda.js 版本

  1. const fetchData = function () {
  2. return Promise.resolve(data);
  3. };
  4. const getIncompleteTaskSummaries = function (membername) {
  5. return fetchData()
  6. .then(R.prop("tasks"))
  7. .then(R.filter(R.propEq("username", membername)))
  8. .then(R.filter(R.propEq("complete", false)))
  9. .then(R.map(R.pick(["id", "dueDate", "title", "priority"])))
  10. .then(R.sortBy(R.prop("dueDate")))
  11. .then(console.log);
  12. };
  13. getIncompleteTaskSummaries("Scott");

使用 compose 的 ramda.js 版本

  1. const fetchData = function () {
  2. return Promise.resolve(data);
  3. };
  4. const getIncompleteTaskSummaries = function (membername) {
  5. return fetchData()
  6. .then(R.compose(
  7. console.log,
  8. R.sortBy(R.prop("dueDate")),
  9. R.map(R.pick(["id", "dueDate", "title", "priority"])
  10. ),
  11. R.filter(R.propEq("complete", false)),
  12. R.filter(R.propEq("username", membername)),
  13. R.prop("tasks"),
  14. ));
  15. };
  16. getIncompleteTaskSummaries("Scott");

参考:

[1] JavaScript专题之函数组合
[2] 完整版例子