『JavaScript函数式编程』备忘录(上)

iz4o4I.jpg

本文建议结合 https://github.com/Tk-archer/functional-javaScript-code-refactoring 这个项目阅读查看

『JavaScript函数式编程』这书其实在几年前我就入手了,但是当时并没能读下去,然后就一直尘封在书柜里面。

直到最近才偶然翻出来,打算好好读一下。本文说是读书笔记的话就过于正式了,所以就只能称之为备忘录了。

为了避免重蹈覆辙,我总结上一次没能读下来的原因

  • 只是读书,书本身不厚,但是里面有不少示例代码,大量的概念直接蕴含着代码当中,而且前后关联,当时顾着硬啃书,而没有去实践一下代码,当然就很难读进去了
  • 代码语法偏差,书中的代码实际上是以 es5 为准实现的,虽然等价,但是习惯了 es6 以上的代码的时候,会明显感受到 es5 所带来的阅读理解成本
  • 类库不一致,本书成型的时候还是 Underscore.js 流行的时候,而我读到这本书的时候已经是 lodash 的时代了,这两者之间的偏差也为我带来了理解上的成本

因此,我所想到的更加好的复读这本书的方法就是—— 用 es6 以上的语法,以及根据自己的理解将示例代码的实现都重构一遍。除了为了让自己阅读起来更加顺畅以外,我觉得重构代码的方式能让我静下心来,更加好地理解其代码中所要表达的思想。

而本文存在的意义便是记录那些在重构过程中的一些想法。

其一,模板字符串和数组拼接字符串

本书从第一章的代码实现开始,就存在着大量的形如 :

1
const str = ['doSomething by' ,something].join(' ')

的代码。这个代码实际上就是一个拼接字符串而已,不过为啥用 array.join() 让我困惑了很久。然后根据我的一些猜测和考证,猜测是这样相当于把字符串封装到数组中, 可以通过一致的方式调用数组的处理函数(如果有需要的话)

因为里面有一句名言:

 用100个函数操作一个数据结构,比用10个函数操作10个数据结构要好。

这是猜测,而考证出来更加实际方面的原因是——在旧时代中的浏览器中用 + 拼接字符串的性能十分糟糕,比如极端的 ie6 环境中能比 array.join() 的方式慢了 6 倍左右,关于这点可以参考这个 提问

因此考虑到成书使时间,使用 array.join() 很有可能是源于作者以前遗留下来的一种“良好”习惯。

不过,现代浏览器不仅有了更好的字符串拼接性能,而且 es6 中也有了更加简洁的语法,所以在我的重构中,上述代码就会变成:

1
const str = `doSomething ${something}`

其二,箭头函数与 rest 解构

这就是其中一个我所说的,阅读体验的问题了,一旦习惯了箭头函数,就很难不觉得满是 function 关键字的代码不够简洁,而本书中,经常会需要操作到 arguments ,有了 rest 析构,则可以进一步精简代码,让我们的注意力集中在需要的地方

1
2
3
4
5
6
7
8
9
10
// old
function getValueBykey(){
var keys = _.toArray(arguments)
return function (obj){
return _.pick(obj, keys)
}
}
// es6
const getValueByKey = (...keys) => obj => _.pick(obj, keys)

箭头函数是个好文明

其三,_.pick 与 construct 与 rest

这个是 ep-2 里的一段代码,很的说明了其相互关联一面

1
2
3
4
5
6
7
const cat = (head, ...rest) => existy(head) ? [...head].concat(...rest) : []
// cat([1],[2,3],[4,5,6]) => [1,2,3,4,5,6]
const construst = (head, tail) => cat([head], Array.from(tail))
// construst(42, [1,2,3]) => [42, 1,2,3]

可以看到 cat 的作用是将所有数组参数合并到一起,而 construst 则是将首参和第二个参数合并到同一个数组中

接着,如果完全按照原文中的逻辑重构,会有这样一段代码

1
2
3
4
const project = (table, keys) => _.map(table, obj => _.pick.apply(null, construct(obj, keys)))
const table = [{name:1,age:2}, {name:3,age:4}, {name:4,age:5}]
project(table, ['name']) // => [{name:1}, {name:3}, {name:4}]

这段代码也让我稍微纠结了一下,尤其是后面调用 construct 的地方有点让人困惑,实际上作者要这样写的原因是

  • underscore 的 pick 的 keys 不支持 第二参数传入数组,只支持将 key 作为参数逐个传入
    • 比如 _.pick({}, key1, key2, key3)
  • 为了展开 keys 数组作为 pick 的参数,所以会使用 apply ,而construct 就是为了将 obj 也拼接到数组中,用作首参
1
2
const params = construct({name:1}, ['name','age']) // => [{name:1}, 'name','age']
_.pick.apply(null, params) // => pick({name:1}, 'name','age')

因此这里有两个精简方法

1
2
3
4
// 一个是直接利用 rest 展开,就不需要特地调用 construct
const project = (table, keys) => _.map(table, obj => _.pick(obj, ...keys))
// 另一种是 lodash _.pick 支持第二参数传入keys数组
const project = (table, keys) => _.map(table, obj => _.pick(obj, keys))

这里同时也体现了 lodash 和 underscore 那些细微的差异,对代码理解时带来的困惑。

其四,chain 是个坏文明

在本书中,作者非常推崇 chain,因为 chain 可以将数据封装到一个一致的数据结构中,然后使用 underscore 中的种种便利的操作函数处理它,这非常好的体现了 用100个函数操作一个数据结构,比用10个函数操作10个数据结构要好 的想法。

1
2
3
4
5
6
_.chain([])
true.push(1)
true.push(2)
true.map(v=> v+1)
true.filter(v=> v>2)
true.value()

然而,在现实中 chain 却不是一个良好的实践方案,它固然有好处,但是他带来的问题也同样明显。

关于这点可以看我翻译的这篇文章,作者详尽的介绍了为啥 chain 不是一个好的习惯

简单在这总结一下就是

  • 为了调用到所有有可能使用到的函数,你需要将所有的函数绑定到原型上,这让类库体积飙增且难以精简。
  • 为了迎合 chain 链式调用的方式,你需要使用一些相对复杂且不友好的方式才能对它进行扩展

因此,在我重构的代码中,我总是选择换一种方式实现,而非 chain,所幸的是我们其实不乏更好的,更加函数式的表达方式可以替代—— compose

1
2
3
4
5
6
flow(
()=>[],
ary => [1,2],
map(v=> v+1),
filter(v=> v>2)
)

结语

本文写在我已经重构了 4 章的代码的时候,大约一半左右,理论上应该还有一篇下,不过那应该是我将所有代码重构一遍之后的事情了。