Table of Contents
Preface
这是一本你可能2小时就能看完的小书,但是涵盖了基本所有函数式编程的内容,还包含了一些 ECMAScript 6 定义的函数式新特性, 如箭头函数, 模式匹配等等. 还会介绍函数式一些重要概念在 JavaScript是如何实现即应用, 以及如何以函数式的思想编写 JavaScript 代码.
对果JavaScript的基本概念对你来说:
可能本书并不适合你, 请先移步 JavaScript Allonge, 但是如果你学习函数式编程时感到:
那么这本书将会对你会有所帮助.
我选用的 JavaScript 函数式库是 Eweda(Ramda 的最初实现,更遵守函数式教条,但由于 javascript 的栈很容易爆,Ramda的实现要更 pratical 一些而且可以用的产品代码中, 千万不要在产品中用 eweda,这里只用eweda做介绍目的)
为什么不用 Underscore 请移步第二章
由于会介绍 ECMAScript 6 的新特性, 书中很多写法都是 ECMAScript 6 标准, 只能在实现这些 feature 的浏览器(如 Firefox, 请目前参照各浏览器的实现情况) 里运行. 另外, 大多数的例子源码都会在文章里的 jsbin 链接里.
Lambda
为什么一开始就讲 lambda, 如果小时候玩过游戏”半条命”,那么你早都见过 lambda 了.
我从wikipedia里面粘出来了这么一段定义:
lambda包括一条变换规则(变量替换)和一条函数定义方式,Lambda演算之通用在于,任何一个可计算函数都能用这种形式来表达和求值。因而,它是等价于图灵机的.
好吧, 跟没解释一样. 简单来说lambda其实就是 x 到 y 的映射关系,
但在大部分支持函数式的编程语言中, 它等价于匿名函数. 被称为 lambda
表达式. 因为这些函数只需要用一次, 而且变换不复杂, 完全不需要命名.
匿名函数在程序中的作用是可以作为参数传给高阶函数1, 或者作为闭包被返回.
但是匿名函数并不是原本的 lambda 算子, 因为匿名函数也可以接受多个参数, 如
1 multiple(x, y) = x*y
写成简单映射的形式, 把名字去掉
1 (x,y) -> x*y
这就是 lambda 了吗, 不是, lambda的用意是简化这个映射关系以至不需要名字, 更重要的是只映射一个 x.
什么意思呢? 让我们来分解一下上面的这个映射的过程.
- lambda 接受第一个参数
5, 返回另一个 lambda1(5)->(y->5*y) - 该返回的 lambda
y -> 5*y接收y并且返回5*y, 若在用4调用该 lambda
1 4 -> 5*4
因此这里的匿名函数 (x,y)->x*y 看似一个 lambda, 其实是两个 lambda
的结合.
而这种接受一个参数返回另一个接收第二个参数的函数叫柯里化2.
这里我们先忍一忍, 来看下 JavaScript 中的 lambda 表达式.
箭头函数(arrow function)
来看看越来越函数式の JavaScript
新的草案ECMAScript 6 (虽然说是草案,但你可以看到 Firefox 其实已经实现大部分的 feature)里我们越来越近了, 借助一下transcompiler例如Babel 我们完全可以在项目中开始使用es6了3。
看看里面有一行 arrow function,为什么叫箭头函数,还记得前面说lambda是提到的箭头吗。而且如果你之前用过 Haskell(单箭头) 或者Scala(双箭头), 会发现都用的是箭头来表示简单映射关系.
由于 arrow function 只在Firefox 22以上版本实现, 本节的所有代码都可以在Firefox的Console中调试, 其他chrome 什么的都没有实现(完全)4. 另外每节的最后我都会给出完整代码的可执行的 jsbin 链接.
声明一个箭头函数
你可以用两种方式定义一个箭头函数
1 ([param] [, param]) => {
2 statements
3 }
4 // or
5 param => expression
单个表达式可以写成一行, 而多行语句则需要 block {} 括起来.
为什么要用箭头函数
看看旧的匿名函数怎么写一个使数组中数字都乘2的函数.
1 var a = [1,2,3,4,5];
2 a.map(function(x){ return x*2 });
用箭头函数会变成
1 a.map(x => x*2);
只是少了 function 和 return 以及 block, 不是吗? 如果觉得差不多,
因为你看惯了 JavaScript 的匿名函数,
你的大脑编译器自动的忽略了,因为他们不需要显示的存在.
而 map(x => x*2) 要更 make sense,
因为我们需要的匿名函数只需要做一件事情, 我们需要的是 一个函数 f,
可以将给定 x, 映射到 y. 翻译这句话的最简单的方式不就是 f (x
> x*2)
Lexical this
如果你觉得这还不足以说服改变匿名函数的写法,
那么想想以前写匿名函数中的经常需要 var self=this 的苦恼.
1 var Multipler = function(inc){
2 this.inc = inc;
3 }
4 Multipler.prototype.multiple = function(numbers){
5 return numbers.map(function(number){
6 return this.inc * number;
7 })
8 }
9 new Multipler(2).multiple([1,2,3,4])
10 // => [NaN, NaN, NaN, NaN] 不 work, 因为 map 里面的 this 指向的是全局变量( window)
11
12 Multipler.prototype.multiple = function(numbers){
13 var self = this; // 保持 Multipler 的 this 的缓存
14 return numbers.map(function(number){
15 return self.inc * number;
16 })
17 }
18 new Multipler(2).multiple([1,2,3,4]) // => [ 2, 4, 6, 8 ]
很怪不是吗, 确实是 Javascript 的一个 bug, 因此经常出现在各种面试题中.
试试替换成 arrow function
1 Multipler.prototype.multiple = function(numbers){
2 return numbers.map((number) => number*this.inc);
3 };
4
5 console.log(new Multipler(2).multiple([1,2,3,4]));// => [ 2, 4, 6, 8 ]
不需要 var self=this 了是不是很开心☺️现在, arrow function 里面的 this
会自动 capture 外层函数的 this 值.
JavaScript的匿名函数(anonymous function)
支持匿名函数, 也就意味着函数可以作为一等公民. 可以被当做参数, 也可以被当做返回值.因此, JavaScript 的支持一等函数的函数式语言, 而且定义一个匿名函数式如此简单.
创建一个匿名函数
在JavaScript里创建一个函数是如此的 ~~简单~~ … 比如:
1 function(x){
2 return x*x;
3 }// => SyntaxError: function statement requires a name
但是, 为什么报错了这里. 因为创建一个匿名函数需要用表达式(function expression). 表达式是会返回值的:
1 var a = new Array() // new Array 是表达式, 而这整行叫语句 statement
但为什么说 function statement requires a name. 因为 JavaScript
还有一种 创建函数的方法–/function statement/.
而在上面这种写法会被认为是一个 function 语句, 因为并没有期待值. 而
function 语句声明是需要名字的.
简单将这个函数赋给一个变量或当参数传都不会报错, 因为这时他没有歧义,只能是表达式.比如:
1 var squareA = function(x){
2 return x*x;
3 }
但是这里比较 tricky 的是这下 squareA 其实是一个具名函数了.
1 console.log(squareA) // => function squareA()
虽然结果是具名函数,但是过程却与下面这种声明的方式不一样.
1 function squareB(x){
2 return x*x;
3 } // => undefined
squareB 用的是 function statement 直接声明(显然 statement 没有返回),
而 squareA 则是先用 function expression 创建一个匿名函数,
然后将返回的函数赋给了名为 squareA 的变量. 因为表达式是有返回的:
1 console.log(function(x){ return x*x});
2 // => undefined
3 // => function ()
第一个 undefined 是 console.log 的返回值, 因此 function()
则是打印出来的 function 表达式创建的匿名函数.
使用匿名函数
JavaScript 的函数是一等函数. 这意味着我们的函数跟值的待遇是一样的,于是它
可以赋给变量:
1 var square = function(x) {return x*x}
可以当参数, 如刚才见到的:
1 console.log(function(x){return x*x})
将函数传给了 console.log
可以被返回:
1 function multiply(x){
2 return function(y){
3 return x*y;
4 }
5 }
6 multiply(1)(2) // => 2
- 第二章会详细解释高阶函数和闭包.↩
- 柯里化会在第二章详细讨论.↩
- 可以看看es6比较有意思的新特性 http://blog.oyanglul.us/javascript/essential-ecmascript6.html↩
- Chrome有一个 feature toggle 可以打开部分 es6 功能 <chrome://flags/#enable-javascript-harmony>↩
高阶函数(Higher-order function)
我们已经见识到了匿名函数和箭头函数的用法, 匿名的一等函数到底有什么用呢? 来看看高阶函数的应用.
高阶函数意思是它接收另一个函数作为参数. 为什么叫 高阶:
来看看这个函数 f(x, y) x(y)= 按照 lambda 的简化过程则是
1 f(x) => (y -> x(y))
2 (y) => x(y)
可以卡出来虽然只是调用 f, 但是返回还有一个函数x要求值.
还记得高等数学里面的导数吗, 两阶以上的导数叫高阶导数. 因为求导一次以后返回的可以求导.
概念是一样的, 如同俄罗斯套娃 当函数执行以后还需执行或者要对参数执行, 因此叫高阶函数.
高阶函数很多典型的应用如 map, reduce.
他们都是以传入不同的函数来以不同的方式操作数组元素.
而柯里化, 则是每次消费一个参数并返回一个逐步被配置好的函数.
高阶函数的这些应用都是为函数的组合提供灵活性. 在本章结束相信你会很好的体会到函数组合的强大之处.
Higher-order function
在 JavaScript 中, 使用高阶函数是非常方便的.
函数作为参数
假设我现在要对一个数组排序, 用我们熟悉的 sort
1 [1,3,2,5,4].sort( (x, y) => x - y )
如果我们要逆序的排序, 把减号左右的 x 和 y 呼唤,就这么简单,
但如果我是一个对象数组, 要根据对象的 id 排序:
1 [{id:1, name:'one'},
2 {id:3, name:'three'},
3 {id:2, name:'two'},
4 {id:5, name:'five'},
5 {id:4, name:'four'}].sort((x,y) => x.id - y.id)
是不是已经能够感受到高阶函数与匿名函数组合的灵活性.
函数作为返回值
函数的返回值可以不只是值, 同样也可以是一个函数, 来看 Eweda
内部的一个工具函数 aliasFor, 他的作用是给函数 E 的一些方法起一些别名:
Note 听起来很怪不是吗, 函数怎么有方法, 实际上 JavaScript 的
function是一个特殊 对象, 试试在 Firefox console 里敲console.log.是不是看到了一些方法, 但是typeof console.log是 function
1 var E = () => {}
2 var aliasFor = oldName => {
3 var fn = newName => {
4 E[newName] = E[oldName];
5 return fn;
6 };
7 return (fn.is = fn.are = fn.and = fn);
8 };
这里有两个 return, 一个是 fn 返回自己, 另一个是 aliasFor 也返回
fn, 并且给 fn 了几个别名 fn.is fn.are…
什么意思呢? fn 返回 fn. 很简单就是 fn() => fn, 那么
fn()()=>fn()=>fn …以此类推, 无论调用 fn 多少次,都最终返回 fn.
这到底有什么用呢, 由于这里使用了 fn 的副作用(side affect)
来干了一些事情 E[newName]=E[oldName], 也就是给 E 的方法起一个别名,
因此每次调用 fn 都会给 E 起一个别名. aliasFor 最后返回的是 fn
自己的一些别名, 使得可以 chain 起来更可读一些:
1 aliasFor('reduce').is('reduceLeft').is('foldl')
另外, 函数作为返回值的重要应用, 柯里化与闭包, 将会在在后面专门介绍. 我们先来看下以函数作为参数的高阶函数的典型应用.
柯里化 currying
还记得 Haskell Curry吗
多巧啊, 人家姓 Curry 名 Haskell, 难怪 Haskell 语言会自动柯里化, 呵呵. 但是不奇怪吗, 为什么要柯里化呢. 为什么如此重要得让 Haskell 会默认自动柯里化所有函数, 不就是返回一个部分配置好的函数吗.
我们来看一个 Haskell 的代码.
1 max 3 4
2 (max 3) 4
结果都是4, 这有什么用呢.
这里看不出来, 放到 高阶函数 试试. 什么? 看不懂天书 Haskell, 来看看
JavaScript 吧.
我们来看一个问题
1. 写一个函数, 可以连接字符数组, 如 f(['1','2']) => '12'
好吧,如果不用柯里化, 怎么写? 啊哈 reduce
1 var concatArray = function(chars){
2 return chars.reduce(function(a, b){
3 return a.concat(b);
4 });
5 }
6 concat(['1','2','3']) // => '123'
很简单,对吧.
2. 现在我要其中所有数字加1, 然后在连接
1 var concatArray = function(chars, inc){
2 return chars.map(function(char){
3 return (+char)+inc + '';
4 }).reduce(function(a,b){
5 return a.concat(b)
6 });
7 }
8 console.log(concatArray(['1','2','3'], 1))// => '234'
3. 所有数字乘以2, 再重构试试看
1 var multiple = function(a, b){
2 return +a*b + ''
3 }
4 var concatArray = function(chars, inc){
5 return chars.map(function(char){
6 return multiple(char, inc);
7 }).reduce(function(a,b){
8 return a.concat(b)
9 });
10 }
11 console.log(concatArray(['1','2','3'], 2)) // => '246'
是不是已经看出问题了呢? 如果我在需要每个数字都减2,是不是很麻烦呢.需要将
map 参数匿名函数中的 multiple 函数换掉. 这样一来 concatArray
就不能同时处理加, 乘和减? 那么怎么能把他提取出来呢?
来对比下柯里化的解法.
柯里化函数接口
1 var multiple = function(a){
2 return function(b){
3 return +b*a + ''
4 }
5 }
6
7 var plus = function(a){
8 return function(b){
9 return (+b)+a + ''
10 }
11 }
12 var concatArray = function(chars, stylishChar){
13 return chars.map(stylishChar)
14 .reduce(function(a,b){
15 return a.concat(b)
16 });
17 }
18 console.log(concatArray(['1','2','3'], multiple(2)))
19 console.log(concatArray(['1','2','3'], plus(2)))
有什么不一样呢 1. 处理数组中字符的函数被提取出来, 作为参数传入 2. 提取成柯里化的函数, 部分配置好后传入, 好处显而易见, 这下接口非常通畅 无论是外层调用
1 concatArray(['1','2','3'], multiple(2))
还是内部的 map 函数
1 chars.map(stylishChar)
这些接口都清晰了很多, 不是吗
这就是函数式的思想, 用已有的函数组合出新的函数, 而柯里化每消费一个参数, 都会返回一个新的部分配置的函数, 这为函数组合提供了更灵活的手段, 并且使得接口更为流畅.
自动柯里化
在 Haskell 语言中, 函数是会自动柯里化的:
1 max 3 4
其实就是
1 (max 3) 4
可以看看 max 与 max 3 函数的 类型
1 ghci> :t max
2 max :: Ord a => a -> a -> a
看明白了么, Ord a => 表示类型约束为可以比较大小的类型, 因此=max=
的类型可以翻译成: 当给定一个=a=, 会得到=a -> a=, 再看看=max
3=的类型就好理解了
1 ghci> :t max 3
2 (Num a, Ord a) => a -> a
左侧表示类型约束 a 可以是 Ord 或者 Num, 意思是 max 3
还是一个函数,如果给定一个 Ord 或者 Num 类型的参数 则返回一个 Ord
或者 Num.
现在是不是清晰了, 在 Haskell 中每给定一个参数, 函数如果是多参数的, 该函数还会返回一个处理余下参数的函数. 这就是自动柯里化.
而在 Javascript(以及大多数语言) 中不是的, 如果给定多参函数的部分参数,
函数会默认其他参数是 undefined, 而不会返回处理剩余参数的函数.
1 function willNotCurry(a, b, c) {
2 console.log(a, b, c)
3 return a*b-c;
4 }
5 willNotCurry(1)
6 // => NaN
7 // => 1 undefined undefined
如果使用自动柯里化的库 eweda, 前面的例子简直就完美了
1 var multiple = curry(function(a, b){
2 return +b*a + ''
3 })
4 var plus = curry(function(a, b){
5 return (+b)+a + ''
6 })
函数组合 function composition
通过前面介绍的高阶函数, map, fold 以及柯里化, 其实已经见识到什么是函数组合了. 如之前例子中的 map 就是 由 fold 函数与 reverse 函数组合出来的.
这就是函数式的思想, 不断地用已有函数, 来组合出新的函数.
如图就是函数组合,来自 Catgory
Theory(Funtor
也是从这来的,后面会讲到), 既然从 A到B 有对应的映射f,B到
C有对应的映射g, 那么 (g.f)(x) 也就是 f 与 g 的组合 g(f(x)) 就是
A到 C 的映射。上一章实现的 map 函数就相当于 reverse.fold.
pipe
类似 compose, eweda/ramda 还有一个方法叫 pipe, pipe 的函数执行方向刚好与
compose 相反. 比如 pipe(f, g), f 会先执行, 然后结果传给 g,
是不是让你想起了 bash 的 pipe
1 find / | grep porno
实际上就是 pipe(find, grep(porno))(/)
没错,他们都是一个意思. 而且这个函数执行的方向更适合人脑编译(可读)一些.
如果你已经习惯 underscore 的这种写法
1 _(data)
2 .chain()
3 .map(data1,fn1)
4 .filter(data2, fn2)
5 .value()
那么转换成 pipe 是很容易的一件事情,而且更简单明了易于重用和组合。
1 pipe(
2 map(fn1),
3 filter(fn2)
4 )(data)