需求

我们需要写一个函数,输入 'kevin',返回 'HELLO, KEVIN'。

尝试

  1. var toUpperCase = function(x) { return x.toUpperCase(); };
  2. var hello = function(x) { return 'HELLO, ' + x; };
  3.  
  4. var greet = function(x){
  5. return hello(toUpperCase(x));
  6. };
  7.  
  8. greet('kevin');

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

优化

试想我们写个 compose 函数:

  1. var compose = function(f,g) {
  2. return function(x) {
  3. return f(g(x));
  4. };
  5. };

greet 函数就可以被优化为:

  1. var greet = compose(hello, toUpperCase);
  2. greet('kevin');

利用 compose 将两个函数组合成一个函数,让代码从右向左运行,而不是由内而外运行,可读性大大提升。这便是函数组合。

但是现在的 compose 函数也只是能支持两个参数,如果有更多的步骤呢?我们岂不是要这样做:

  1. compose(d, compose(c, compose(b, a)))

为什么我们不写一个帅气的 compose 函数支持传入多个函数呢?这样就变成了:

  1. compose(d, c, b, a)

compose

我们直接抄袭 underscore 的 compose 函数的实现:

  1. function compose() {
  2. var args = arguments;
  3. var start = args.length - 1;
  4. return function() {
  5. var i = start;
  6. var 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.  
  3. // 非 pointfree,因为提到了数据:name
  4. var greet = function(name) {
  5. return ('hello ' + name).toUpperCase();
  6. }
  7.  
  8. // pointfree
  9. // 先定义基本运算,这些可以封装起来复用
  10. var toUpperCase = function(x) { return x.toUpperCase(); };
  11. var hello = function(x) { return 'HELLO, ' + x; };
  12.  
  13. var greet = compose(hello, toUpperCase);
  14. greet('kevin');

我们再举个稍微复杂一点的例子,为了方便书写,我们需要借助在《JavaScript专题之函数柯里化》中写到的 curry 函数:

  1. // 需求:输入 'kevin daisy kelly',返回 'K.D.K'
  2.  
  3. // 非 pointfree,因为提到了数据:name
  4. var initials = function (name) {
  5. return name.split(' ').map(compose(toUpperCase, head)).join('. ');
  6. };
  7.  
  8. // pointfree
  9. // 先定义基本运算
  10. var split = curry(function(separator, str) { return str.split(separator) })
  11. var head = function(str) { return str.slice(0, 1) }
  12. var toUpperCase = function(str) { return str.toUpperCase() }
  13. var join = curry(function(separator, arr) { return arr.join(separator) })
  14. var map = curry(function(fn, arr) { return arr.map(fn) })
  15.  
  16. var initials = compose(join('.'), map(compose(toUpperCase, head)), split(' '));
  17.  
  18. initials("kevin daisy kelly");

从这个例子中我们可以看到,利用柯里化(curry)和函数组合 (compose) 非常有助于实现 pointfree。

也许你会想,这种写法好麻烦呐,我们还需要定义那么多的基础函数……可是如果有工具库已经帮你写好了呢?比如 ramda.js

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

而且你也会发现:

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

那么使用 pointfree 模式究竟有什么好处呢?

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

实战

这个例子来自于 Favoring Curry

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

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

我们需要写一个名为 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. // 第一版 过程式编程
  2. var fetchData = function() {
  3. // 模拟
  4. return Promise.resolve(data)
  5. };
  6.  
  7. var getIncompleteTaskSummaries = function(membername) {
  8. return fetchData()
  9. .then(function(data) {
  10. return data.tasks;
  11. })
  12. .then(function(tasks) {
  13. return tasks.filter(function(task) {
  14. return task.username == membername
  15. })
  16. })
  17. .then(function(tasks) {
  18. return tasks.filter(function(task) {
  19. return !task.complete
  20. })
  21. })
  22. .then(function(tasks) {
  23. return tasks.map(function(task) {
  24. return {
  25. id: task.id,
  26. dueDate: task.dueDate,
  27. title: task.title,
  28. priority: task.priority
  29. }
  30. })
  31. })
  32. .then(function(tasks) {
  33. return tasks.sort(function(first, second) {
  34. var a = first.dueDate,
  35. b = second.dueDate;
  36. return a < b ? -1 : a > b ? 1 : 0;
  37. });
  38. })
  39. .then(function(task) {
  40. console.log(task)
  41. })
  42. };
  43.  
  44. getIncompleteTaskSummaries('Scott')

如果使用 pointfree 模式:

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

如果直接使用 ramda.js,你可以省去编写基本函数:

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

当然了,利用 compose,你也可以这样写:

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

compose 是从右到左依此执行,当然你也可以写一个从左到右的版本,但是从右向左执行更加能够反映数学上的含义。

ramda.js 提供了一个 R.pipe 函数,可以做的从左到右,以上可以改写为:

  1. // 第五版 使用 R.pipe
  2. var getIncompleteTaskSummaries = function(membername) {
  3. return fetchData()
  4. .then(R.pipe(
  5. R.prop('tasks'),
  6. R.filter(R.propEq('username', membername)),
  7. R.filter(R.propEq('complete', false)),
  8. R.map(R.pick(['id', 'dueDate', 'title', 'priority'])
  9. R.sortBy(R.prop('dueDate')),
  10. console.log,
  11. ))
  12. };

专题系列

JavaScript专题系列目录地址:https://github.com/mqyqingfeng/Blog

JavaScript专题系列预计写二十篇左右,主要研究日常开发中一些功能点的实现,比如防抖、节流、去重、类型判断、拷贝、最值、扁平、柯里、递归、乱序、排序等,特点是研(chao)究(xi) underscore 和 jQuery 的实现方式。

如果有错误或者不严谨的地方,请务必给予指正,十分感谢。如果喜欢或者有所启发,欢迎 star,对作者也是一种鼓励。