【大前端攻城狮之路】JavaScript函数式编程
转眼之间已入五月,自己毕业也马上有三年了。大学计算机系的同学大多都在北京混迹,大家为了升职加薪,娶媳妇买房,熬夜加班跟上线,出差pk脑残客户。同学聚会时有不少兄弟已经体重飙升,开始关注13号地铁线上铺天盖地的植发广告。都说25岁是一个男人的分界线,之前是越活越精致,往后是越活越糙。现在体会到了。父母开始老去,自己一无所有,攒的钱不够买一平米的房。昨天和肖总撸串,我问:有啥打算?肖总吞了几口羊肉串,喝了一口啤酒,说:存点钱吧,然后回家。
说实话,我以前的想法也一样。奈何来北京容易,想走却很难。清明节回太原一趟,总觉的路上太过于寂静,大家走路速度太慢,商店关门太早,竟然有些许不适应。兀的发觉,北京肉体虽然每天很疲惫,但灵魂力量却修炼的很强。回到昌平的20平出租屋内,内心暗想,继续混,混到混不下去为止。
这是一个系列博客,希望他能在我的码农生涯中留下些什么。
闲话不多说,这篇文章主要和大家分析下前端的函数式编程思想。纲要如下:
-
函数式编程思维
-
函数式编程常用核心概念
-
当下函数式编程最热的库
-
函数式编程的实际应用场景
一 函数式编程思维
范畴论 Category Theory
对于前端,所有的成员是一个集合,变形关系是函数。
函数式编程基础理论
1..函数式编程(Functional Programming)其实相对于计算机的历史而言是一个非常古老的概念,甚至早于第一台计算机的诞生。函数式编程的基础模型来源于 λ (Lambda x=>x*2)演算,而 λ 演算并非设计于在计算机上执行,它是在 20 世纪三十年代引入的一套用于研究函数定义、函数应用和递归的形式系统。
2.函数式编程不是用函数来编程,也不是传统的面向过程编程。主旨在于将复杂的函数符合成简单的函数(计算理论,或者递归论,或者拉姆达演算)。运算过程尽量写成一系列嵌套的函数调用
3.JavaScript 是披着 C 外衣的 Lisp。
4.真正的火热是随着React的高阶函数而逐步升温。
5.函数是一等公民。所谓”第一等公民”(first class),指的是函数与其他数据类型一样,处于平等地位,可以赋值给其他变量,也可以作为参数,传入另一个函数,或者作为别的函数的返回值。
6.不可改变量。在函数式编程中,我们通常理解的变量在函数式编程中也被函数代替了:在函数式编程中变量仅仅代表某个表达式。这里所说的’变量’是不能被修改的。所有的变量只能被赋一次初值。
7.map & reduce他们是最常用的函数式编程的方法。
将上面的概念简述一下:
1. 函数是”第一等公民”
2. 只用”表达式",不用"语句"
3. 没有”副作用"
4. 不修改状态
5. 引用透明(函数运行只靠参数)
二 函数式编程常用核心概念
•纯函数
•函数的柯里化
•函数组合
•Point Free
•声明式与命令式代码
•核心概念
1.纯函数
什么是纯函数呢?
对于相同的输入,永远会得到相同的输出,而且没有任何可观察的副作用,也不依赖外部环境的状态的函数,叫做纯函数。
举个栗子:
var xs = [1,2,3,4,5];// Array.slice是纯函数,因为它没有副作用,对于固定的输入,输出总是固定的 xs.slice(0,3); xs.slice(0,3); xs.splice(0,3);// Array.splice会对原array造成影响,所以不纯 xs.splice(0,3);
2.函数柯里化
传递给函数一部分参数来调用它,让它返回一个函数去处理剩下的参数。
我们有这样一个函数checkage:
var min = 18;
var checkage = age => age > min;
这个函数并不纯,checkage 不仅取决于 age还有外部依赖的变量 min。 纯的 checkage 把关键数字 18 硬编码在函数内部,扩展性比较差,柯里化优雅的函数式解决。
var checkage = min => (age => age > min);
var checkage18 = checkage(18); // 先将18作为参数,去调用此函数,返回一个函数age => age > 18;
checkage18(20);// 第二步,上面返回的函数去处理剩下的参数,即 20 => 20 > 18; return true;
// 柯里化之前 function add(x, y) { return x + y; } add(1, 2) // 3 // 柯里化之后 function addX(y) { return function (x) { return x + y; }; } addX(2)(1) // 3
3.函数组合
为了解决函数嵌套过深,洋葱代码:h(g(f(x))),我们需要用到“函数组合”,我们一起来用柯里化来改他,让多个函数像拼积木一样。
const compose = (f, g) => (x => f(g(x))); var first = arr => arr[0]; var reverse = arr => arr.reverse(); var last = compose(first, reverse); last([1, 2, 3, 4, 5]); // 5
函数组合交换律,类似于乘法交换律:
4.Point Free
const f = str => str.toUpperCase().split(' ');
var toUpperCase = word => word.toUpperCase(); var split = x => (str => str.split(x)); var f = compose(split(' '), toUpperCase); f("abcd efgh");
把一些对象自带的方法转化成纯函数,然后通过函数组合去调用,这种风格能够帮助我们减少不必要的命名,让代码保持简洁和通用。是不是很方便!
5.声明式与命令式代码
在我们日常业务开发中,写的代码绝大多数都为命令式代码;
//命令式 let CEOs = []; for (var i = 0; i < companies.length; i++) { CEOs.push(companies[i].CEO) } //声明式 let CEOs = companies.map(c => c.CEO);
6.核心概念
下面我们再深入一下,大家注意好好理解吸收:
高阶函数
高阶函数,就是把函数当参数,把传入的函数做一个封装,然后返回这个封装函数,达到更高程度的抽象。
//命令式 var add = function (a, b) { return a + b; }; function math(func, array) { return func(array[0], array[1]); } math(add, [1, 2]); // 3
递归与尾递归
// 不是尾递归,无法优化 function factorial(n) { if (n === 1) return 1; return n * factorial(n - 1); } function factorial(n, total) { if (n === 1) return total; return factorial(n - 1, n * total); } //ES6强制使用尾递归
我们看一下递归和尾递归执行过程:
递归:
function sum(n) { if (n === 1) return 1; return n + sum(n - 1); }
sum(5) (5 + sum(4)) (5 + (4 + sum(3))) (5 + (4 + (3 + sum(2)))) (5 + (4 + (3 + (2 + sum(1))))) (5 + (4 + (3 + (2 + 1)))) (5 + (4 + (3 + 3))) (5 + (4 + 6)) (5 + 10) 15 // 递归非常消耗内存,因为需要同时保存很多的调用帧,这样,就很容易发生“栈溢出”
尾递归
function sum(x, total) { if (x === 1) { return x + total; } return sum(x - 1, x + total); }
sum(5, 0) sum(4, 5) sum(3, 9) sum(2, 12) sum(1, 14) 15
范畴与容器
1.函数不仅可以用于同一个范畴之中值的转换,还可以用于将一个范畴转成另一个范畴。这就涉及到了函子(Functor)。
2.函子是函数式编程里面最重要的数据类型,也是基本的运算单位和功能单位。它首先是一种范畴,也就是说,是一个容器,包含了值和变形关系。比较特殊的是,它的变形关系可以依次作用于每一个值,将当前容器变形成另一个容器。
容器与函子(Functor)
$(...) 返回的对象并不是一个原生的 DOM 对象,而是对于原生对象的一种封装,这在某种意义上就是一个“容器”(但它并不函数式)。
Functor(函子)遵守一些特定规则的容器类型。任何具有map方法的数据结构,都可以当作函子的实现。
Functor 是一个对于函数调用的抽象,我们赋予容器自己去调用函数的能力。把东西装进一个容器,只留出一个接口 map 给容器外的函数,map 一个函数时,我们让容器自己来运行这个函数,这样容器就可以自由地选择何时何地如何操作这个函数,以致于拥有惰性求值、错误处理、异步调用等等非常牛掰的特性。
下面我们看下函子的代码实现:
var Container = function (x) { this.__value = x; } // 函数式编程一般约定,函子有一个of方法 Container.of = x => new Container(x); // Container.of(‘abcd’); // 一般约定,函子的标志就是容器具有map方法。该方法将容器 // 里面的每一个值, 映射到另一个容器。 Container.prototype.map = function (f) { return Container.of(f(this.__value)) } Container.of(3) .map(x => x + 1) //=> Container(4) .map(x => 'Result is ' + x); //=> Container('Result is 4')
class Functor { constructor(val) { this.val = val; } map(f) { return new Functor(f(this.val)); } } (new Functor(2)).map(function (two) { return two + 2; }); // Functor(4)
Functor.of = function (val) { return new Functor(val); }; Functor.of(2).map(function (two) { return two + 2; }); // Functor(4)
下面我们介绍一些常用的函子。
Maybe 函子
var Maybe = function (x) { this.__value = x; } Maybe.of = function (x) { return new Maybe(x); } Maybe.prototype.map = function (f) { return this.isNothing() ? Maybe.of(null) : Maybe.of(f(this.__value)); } Maybe.prototype.isNothing = function () { return (this.__value === null || this.__value === undefined); } //新的容器我们称之为 Maybe(原型来自于Haskell,Haskell是通用函数式编程语言)
Functor.of(null).map(function (s) { return s.toUpperCase(); }); // TypeError Maybe.of(null).map(function (s) { return s.toUpperCase(); }); // Maybe(null)
错误处理、Either函子
我们的容器能做的事情太少了,try/catch/throw 并不是“纯”的,因为它从外部接管了我们的函数,并且在这个函数出错时抛弃了它的返回值。Promise 是可以调用 catch 来集中处理错误的。事实上 Either 并不只是用来做错误处理的,它表示了逻辑或。
条件运算if...else是最常见的运算之一,函数式编程里面,使用 Either 函子表达。Either 函子内部有两个值:左值(Left)和右值(Right)。右值是正常情况下使用的值,左值是右值不存在时使用的默认值。
class Either extends Functor { constructor(left, right) { this.left = left; this.right = right; } map(f) { return this.right ? Either.of(this.left, f(this.right)) : Either.of(f(this.left), this.right); } } Either.of = function (left, right) { return new Either(left, right); };
使用Either函子:
var addOne = function (x) { return x + 1; }; Either.of(5, 6).map(addOne); // Either(5, 7); Either.of(1, null).map(addOne); // Either(2, null); Either .of({ address: 'xxx' }, currentUser.address) .map(updateField);
AP函子
class Ap extends Functor { ap(F) { return Ap.of(this.val(F.val)); } }
function addOne(x) { return x + 1; }
Ap.of(addOne).ap(Functor.of(1)) // ap函子,让addOne可以用后面函子中的val运算 结果为Ap(2)
IO函子
class IO extends Monad { map(f) { return IO.of(compose(f, this.__value)) } }
在这里,我们提到了Monad,Monad就是一种设计模式,表示将一个运算过程,通过函数拆解成互相连接的多个步骤。你只要提供下一步运算所需的函数,整个运算就会自动进行下去。Promise 就是一种 Monad。Monad 让我们避开了嵌套地狱,可以轻松地进行深度嵌套的函数式编程,比如IO和其它异步任务。
class Monad extends Functor { join() { return this.val; } flatMap(f) { return this.map(f).join(); } }
关于更多的Monad介绍,可以移步知乎什么是Monad。
三 流行的几大函数式编程库
1.Rxjs
var clicks = Rx.Observable .fromEvent(document, 'click') .bufferCount(2) .subscribe(x => console.log(x)); // 打印出前2次点击事件
2.Cycle.js
function main(sources) { const sinks = { DOM: sources.DOM.select('input').events('click') .map(ev => ev.target.checked) .startWith(false) .map(toggled => <div> <input type="checkbox" /> Toggle me <p>{toggled ? 'ON' : 'off'}</p> </div> ) }; return sinks; } const drivers = { DOM: makeDOMDriver('#app') }; run(main, drivers);
3.Underscore.js
4.Lodash.js
var abc = function (a, b, c) { return [a, b, c]; }; var curried = _.curry(abc); curried(1)(2)(3);
function square(n) {
return n * n;
}
var addSquare = _.flowRight(square, _.add); // 类似于上面说的函数组合
addSquare(1, 2);
// => 9