Jamey's Jamey's
首页
导航站
  • 学习专栏

    • 《HTML》笔记
    • 《CSS》笔记
    • 《JavaScript》笔记
    • 《Vue》笔记
    • 《Git》笔记
    • 《规范》笔记
    • 《软技能》笔记
    • 《面试》笔记
    • 《持续集成&交付&部署》笔记
  • 踩坑专栏

    • 《Element-UI 实践系列》笔记
    • 《移动端 实践系列》笔记
    • 《综合》笔记
  • 配置专栏

    • 《环境系列》笔记
  • 极空间

    • Docker
  • 影视

    • movie
  • 编辑器笔记

    • 开发编辑器
  • 浏览器笔记

    • Chrome
  • Mac笔记

    • Mac
  • 跨界学习

    • 运营
  • 破解合集

    • 破解
  • 本站

    • 分类
    • 标签
    • 归档
  • 我的

    • 收藏
    • 书单
    • 关于

Jamey

首页
导航站
  • 学习专栏

    • 《HTML》笔记
    • 《CSS》笔记
    • 《JavaScript》笔记
    • 《Vue》笔记
    • 《Git》笔记
    • 《规范》笔记
    • 《软技能》笔记
    • 《面试》笔记
    • 《持续集成&交付&部署》笔记
  • 踩坑专栏

    • 《Element-UI 实践系列》笔记
    • 《移动端 实践系列》笔记
    • 《综合》笔记
  • 配置专栏

    • 《环境系列》笔记
  • 极空间

    • Docker
  • 影视

    • movie
  • 编辑器笔记

    • 开发编辑器
  • 浏览器笔记

    • Chrome
  • Mac笔记

    • Mac
  • 跨界学习

    • 运营
  • 破解合集

    • 破解
  • 本站

    • 分类
    • 标签
    • 归档
  • 我的

    • 收藏
    • 书单
    • 关于
  • 深入系列

  • 专题系列

    • 写在前面
    • JavaScript 专题之跟着 underscore 学防抖
    • JavaScript 专题之跟着 underscore 学节流
    • JavaScript 专题之数组去重
    • JavaScript 专题之类型判断(上)
    • JavaScript 专题之类型判断(下)
    • JavaScript 专题之深浅拷贝
    • JavaScript 专题之从零实现 jQuery 的 extend
    • JavaScript 专题之如何求数组的最大值和最小值
    • JavaScript 专题之数组扁平化
    • JavaScript 专题之学 underscore 在数组中查找指定元素
    • JavaScript 专题之 jQuery 通用遍历方法 each 的实现
    • JavaScript 专题之如何判断两个对象相等
    • JavaScript 专题之函数柯里化
    • JavaScript 专题之偏函数
    • JavaScript 专题之惰性函数
    • JavaScript 专题之函数组合
      • 一. 需求
      • 二. 尝试
      • 三. 优化
      • 四. compose
      • 五. pointfree
      • 六. 实战
    • JavaScript 专题之函数记忆
    • JavaScript 专题之递归
    • JavaScript 专题之乱序
    • JavaScript 专题之解读 v8 排序源码
  • underscore系列

  • ES6系列

  • 模块化

  • 正则表达式

  • 单元测试

  • 微前端

  • 实用函数

  • Rollup

  • 解决方案

  • 《JavaScript》笔记
  • 专题系列
Jamey
2021-11-05
目录

JavaScript 专题之函数组合

# JavaScript 专题之函数组合

# 一. 需求

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

# 二. 尝试

var toUpperCase = function(x) { return x.toUpperCase(); };
var hello = function(x) { return 'HELLO, ' + x; };

var greet = function(x){
  return hello(toUpperCase(x));
};

greet('kevin');
1
2
3
4
5
6
7
8

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

# 三. 优化

试想我们写个 compose 函数:

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

greet 函数就可以被优化为:

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

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

但是现在的 compose 函数也只是能支持两个参数,如果有更多的步骤呢?这样就变成了:

compose(d, compose(b, a));
1

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

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

# 四. compose

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

function compose() {
  var i = start;
  var result = args[start].apply(this, arguments);
  while (i--) result = args[i].call(this, result);
  return result;
}
1
2
3
4
5
6

现在的 compose 函数已经可以支持多个函数了,然而有了这个又有什么用呢?

在此之前,我们先了解一个概念叫做 pointfree。

# 五. pointfree

pointfree 指的是 函数无序无需提及将要操作的数据是什么样的。依然是以最初的需求为例:

// 需求:输入 'kevin',返回 'HELLO, KEVIN'。

// 非 pointfree,因为提到了数据:name
var greet = function(name) {
  return ('hello' + name).toUpperCase();
}

// pointfree
// 先定义基本运算,这些可以封装起来复用
var toUpperCase = function (x) { return x.toUpperCase(); };
var hello = function (x) { return 'HELLO,' + x; };

var greet = compose(hello, toUpperCase);
greet('kevin');
1
2
3
4
5
6
7
8
9
10
11
12
13
14

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

// 需求:输入 'kevin daisy kelly',返回 'K.D.K'

// 非 pointfree,因为提到了数据:name
var initials = function (name) {
  return name.split(' ').map(compose(toUpperCase, head)).join('. ');
}

// pointfree
// 先定义基本运算
var split = curry(function(separator, str) { return str.split(separator) });
var head = function (str) { return str.slice(0, 1) };
var toUpperCase = function (str) { return str.toUpperCase() };
var join = curry(function (separator, arr) { return arr.join(separator) });
var map = curry(function (fn, arr) { return arr.map(fn) });

var initials = compose(join('.'), map(compose(toUpperCase, head)), split(' '));

initials("kevin daisy kelly");
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

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

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

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

而且你会发现:

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

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

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

# 六. 实战

这个例子来自于 Favoring Curry (opens new window) :

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

var 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"}
    ]
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

我们需要写一个名为 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" }
]
1
2
3
4

普通的方式为:

// 第一版 过程式编程
var fetchData = function() {
  // 模拟
  return Promise.resolve(data)
};

var 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) {
         var a = first.dueDate,
             b = second.dueDate;
         return a < b ? -1 : a > b ? 1 : 0;
      });
   })
   .then(function(task) {
     console.log(task);
   })
};

getIncompleteTaskSummaries('Scott');
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44

如果使用 pointfree 模式:

// 第二版 pointfree 改写
var fetchData = function() {
  return Promise.resolve(data);
};

// 编写基本函数
var prop = curry(function(name, obj) {
  return obj[name];
});

var propEq = curry(function(name, val, obj) {
  return obj[name] === val;
});

var filter = curry(function(fn, arr) {
  return arr.filter(fn);
});

var map = curry(function(fn, arr) {
  return arr.map(fn);
});

var pick = curry(function(args, obj){
  var result = {};
  for (var i = 0; i < args.length; i++) {
    result[args[i]] = obj[args[i]];
  }
  return result;
});

var sortBy = curry(function(fn, arr) {
  return arr.sort(function(a, b){
    var a = fn(a),
      b = fn(b);
    return a < b ? -1 : a > b ? 1 : 0;
  })
});

var 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');
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49

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

// 第三版 使用 ramda.js
var fetchData = function() {
  return Promise.resolve(data);
};

var 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');
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

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

// 第四版 使用 compose
var fetchData = function() {
  return Promise.resolve(data);
};

var 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

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

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

// 第五版 使用 R.pipe
var getIncompleteTaskSummaries = function(membername) {
  return fetchData()
    .then(R.pipe(
      R.prop('tasks'),
      R.filter(R.propEq('username', membername)),
      R.filter(R.propEq('complete', false)),
      R.map(R.pick(['id', 'dueDate', 'title', 'priority']),
      R.sortBy(R.prop('dueDate')),
      console.log,
    ))
};
1
2
3
4
5
6
7
8
9
10
11
12
#JavaScript 专题
上次更新: 2022/07/01, 17:34:19
JavaScript 专题之惰性函数
JavaScript 专题之函数记忆

← JavaScript 专题之惰性函数 JavaScript 专题之函数记忆→

Theme by Vdoing | Copyright © 2017-2023 Jamey | blog 闽ICP备19022664号
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式