Js函数式编程

导读

这篇文章是对《JavaScript 函数式编程》这本书的总结。

这本书让我感觉,函数式编程不是为了面向对象编程,也不是软件工程的银弹。只是一种新的视角。

自我感觉这本2013年著作的小册子,有一些地方已经脱离了时代。但是我希望通过这本书 + JS 来了解函数式编程的一些思想。也算是为后期更深入的接触函数式编程打下一定的基础。

这个里面我会加入一些自己的理解,不过因为本人有一点菜,很多个人理解可能存在一定的偏颇,所以读者仅应将我的个人理解作为参考,仍需自己理解与求证。

如果让我自己推荐的话,我不会推荐再去读这本书了。但是如果有一定的时间,也可以自己翻一翻这本小册子,感受一下作者的思维。

函数式编程

作者这里用了很简单的一句话描述了函数式编程:

函数式编程就通过使用函数将数值转换成抽象单元,接着用于构建软件系统。

这里我抛出看了前三章时候自己对函数式编程的一些理解。

我认为函数式编程的本质就是通过函数抽象将目标问题分解,在可读性与抽象性之间达到一定的平衡。

正文喽

从一个小 Demo 领略一下下魅力

假设我们有一个表数据:

title isbn ed
title1 01029394 1
title2 01029395 2
title1 01029396 1

转成JSON大概是下面这样:

1
2
3
4
5
const table = [
{ title: "title1", isbn: "01029394", ed: 1 },
{ title: "title2", isbn: "01029395", ed: 2 },
{ title: "title1", isbn: "01029396", ed: 1 },
];

我们要做的肥肠简单,实现一个类似SQL的抽象

比如:

select title from table;

我们应该如何将它使用函数抽象呢?这里设计一个简单的函数project

如下:

1
2
3
4
5
6
7
8
9
const project = (table, keys) => {
return _.map(table, (o) => {
return _.pick(o, keys)
})
}

// 使用方法:
project(table, ['table']);
// => [ { tit: 'title1' }, { tit: 'title2' }, { tit: 'title1' } ]

嗯,就是这样了

那么如何实现这么等价的抽象呢:select title as tit, isbn from table;

这里我们可以在不改变上一个project的函数,先来实现一个rename函数,功能只需要简单的将指定的key更新就可以了:

1
2
3
4
5
6
7
8
9
const rename = (obj, newNames) => {
return _.reduce(newNames, (o, nu, old) => {
if(_.has(obj, old)){
return o[nu] = obj[old]
}else{
return o
}
}, _.omit(table, _.keys(newNames)))
}

然后as的本质就是对每一行数据,执行这个rename方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const as = (table, newNames) => {
return _.map(table, (line) => {
rename(line, newNames);
})
}

// 使用:
project(as(
table, { title: "tit" }
), ['tit', 'isbn'])
// => 效果:
/*
[ { tit: 'title1', isbn: '01029394' },
{ tit: 'title2', isbn: '01029395' },
{ tit: 'title1', isbn: '01029396' } ]
*/

那么再进阶一下下:

select title as tit, ed as edition from table where edition > 1

这个怎么抽象呢?

可以通过restrict方法进行过滤:

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
const restrict = (table, condition) => {
const _.reduce(
table,
(newTable, line) => {
if(!!condition(line)){
// 符合条件
return newTable;
}else{
return _.without(newTable, line);
}
}
)
}

// 使用:
restrict(
project(
as(
table,
{title: "tit", ed: "edition"}
),
['tit, edition']
),
line => line.edition > 1
)

emmm,说实话,我觉得比起命令式编程,这中抽象不是很好理解,但是也可以从中窥得一丝函数式的魅力——良好的模块化与抽象复用能力。

在业务多变的前端场景中,可复用也是一个很重要的点。

下面我们正式开始学习啦!!!

高阶函数

定义:

  • 函数是一等公民
  • 以一个函数作为参数
  • 以一个函数作为返回结果

以一个函数作为参数

为了使一个函数更加通用,我们将使用一个函数而不是值,下面摘抄本书中的这个例子:

1
const repeat = (times, value) => _.map(_.range(times), () => value); 

=> 为了更通用,我们转换为下面的设计

1
const repeatedly = (times, fn) => _.map(_.range(times), fn);

对比两种实现的优势:

1
2
3
4
5
6
7
8
9
repeat(4, 1) 		   //=> [1,1,1,1]
repeatedly(4, () => 1) //=> [1,1,1,1]
/*但是repeatedly可以做到下面的事情*/
repeatedly(4, () => Math.floor(Math.random()*10) + 1; // => [7,4,9,6]]
repeatedly(4, (idx) => {
const id = `id${idx}`
$('body').append(`<p id=${idx}>odelay~</p>`)
return id
}) // => [id1, id2, id3, id4]

也就是说,将一个值重复多次不如将一个运算重复多次

记得上面的话,使用函数而不是值,times参数仍然存在它的局限性,比如我们如果希望可以有条件的对数值进行迭代,repeatedly函数无疑难以满足我的期待。

所以我们将这个函数优化一下。

1
2
3
4
5
6
7
8
9
const iterateUtil = (fn, check, init) => {
const ret = []
let result = fn(init);
while(check(result)) {
result = fn(result)
ret.push(result)
}
return ret
}

看看效果:

1
2
iterateUtil((n) => n+n, (n) => n < 1024, 1);
//=>[ 4, 8, 16, 32, 64, 128, 256, 512, 1024 ]

返回结果是一个函数

// TODO 第五章讨论

通过invoker实现多态

引用透明

看一个例子吧:

1
2
3
4
5
6
7
8
9
10
11
12
13
const omgenerator = (
function() {
var COUNT = 0
return {
uniqueString(prefix) {
return [prefix, COUNT++].join('')
}
}
}
)(0)

omgenerator.uniqueString('prefix-') => 'prefix-0'
omgenerator.uniqueString('prefix-') => 'prefix-1'

这个函数乍看没有什么太大的问题。COUNT很好的被隐藏,对调用者来说,不会破坏其良好的封装性。

但是这里我们介绍一个概念:引用透明——它是指,一个函数的结果(返回值)仅依赖于参数。

引用透明的最大好处就是使得函数具有可预知性。

这时我们再来看上面的封装,uniqueString这个函数,除了依赖于prefix外,还依赖了一个COUNT变量(状态)即函数的调用次数。

一个不可控的状态是极为危险的。

如果我们需要对这个方法进行测试,我们需要首先知道这个方法的调用次数或其他可能改变COUNT这个状态的操作,才能对结果有所预期,但是这显然是不可能的。

显然,这种情况是必须要避免的,下面来讨论如何避免这种情况?

闭包陷阱

// TODO 第七章

函数参数默认值

假设有这么个情况:

1
2
_.reduce([1,2,3, null], (total, n) => total * n);
// => NaN

这里,数组的null会影响我们得出的结果,所以需要有一个函数fnull来为参数的null提供一些默认值。

下面我们看看它的实现:

1

查看评论