现在大公司的编程方式有:
1.oop(面向对象编程);
2.aop(面向切面编程);
3.函数式编程(JavaScript Functional Programming);
范畴论Category Theory
- 函数式编程是范畴论的数学分支是一门很复杂的数学,认为世界上所有概念体系都可以抽象出一个个范畴
- 彼此之间存在某种关系概念、事物、对象等等,都构成范畴。任何事物只要找出他们之间的关系,就能定义
- 箭头表示范畴成员之间的关系,正式的名称叫做“态射”(morphism)。范畴论认为,同一个范畴的所有成员,就是不同状态的“变形”(transformation)。通过“态射”,一个成员可以变形成另一个成员
函数式编程5大特点
- 函数是
第一等公民
- 只用
表达式
,不用语句
- 没有
副作用
- 不修改状态
- 引用透明(函数运行只靠参数)
专业术语
- 纯函数
- 函数的柯里化
- 函数组合
- Point Free
- 声明式命令式代码
- 惰性求值
纯函数
对于相同的输入,永远会得到相同的输出,而且没有任何可观察的副作用,也不依赖外部环境的状态。
var xs=[1,2,3,4,5];
//Array.slice是纯函数,因为它没有副作用,对于固定的输入,输出总是固定的
xs.slice(0,3); //[1,2,3]
xs.slice(0,3); //[1,2,3]
xs.splice(0,3); //[1,2,3]
xs.splice(0,3); //[4,5]
import _ from 'lodash';
var sin=_.memorize(x=>Math.sin(x));
var a=sin(1); //第一次计算的时候会稍慢一点
var b=sin(1); //第二次有了缓存,速度极快
//纯函数不仅可以有效降低系统的复杂度,还有很多很棒的特性,比如可缓存性
//惰性函数
不纯
var min=18;
var checkage=function(age){
return age>min; //依赖于外部的min,导致不纯
}
函数的柯里化
传递给函数一部分参数来调用它,让它返回一个函数去处理剩下的参数。
用柯里化来改造上面的不纯函数
var checkage=min=>(age=>age>min);
var checkage18=checkage(18);
checkage18(20);
Point Free
- 把一些对象自带的方法转化成纯函数,不要命名转瞬即逝的中间变量
- 这个函数中,我们使用了
str
作为我们的中间变量,但这个中间变量除了让代码变得长了一点以外是毫无意义的
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");
这种风格能够帮助我们减少不必要的命名,让代码保持简洁和通用
声明式与命令式代码
命令式代码的意思就是,我们通过编写一条又一条指令去让计算机执行一些动作,这其中一般都会涉及到很多繁杂的细节。而声明式就要优雅很多了,我们通过写表达式的方式来声明我们想干什么,而不是通过一步一步的指示。
//命令式
let CEOs=[];
for(var i=0;i<companies.length;i++){
CEOs.push(companies[i].CEO);
}
//声明式
let CEOs=companies.map(c=>c.CEO);
优缺点
函数式编程的一个明显的好处就是这种声明式的代码,对于无副作用的纯函数,我们完全可以不考虑函数内部是如何实现的,专注于编写业务代码。优化代码时,目光只需要集中在这些稳定坚固的函数内部即可。
相反,不纯的函数式的代码会产生副作用或者依赖外部系统环境,使用他们的时候总是要考虑这些不干净的副作用。在复杂的系统中,这对于程序员的心智来说是极大的负担。
惰性求值
function fn(){
if(IE){//IE时
fn=a;
}else{//chrome时
fn=b;
}
return fn;
}
第一次执行时会走if,然后fn重新赋值,第二次执行fn时,直接赋值不用判断,提高执行效率
高阶函数
函数当参数,把传入的函数做一个封装,然后返回这个封装函数,达到更高程度的抽象
//命令式
var add=function(a,b){
return a+b;
};
funtion 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强制使用尾递归
普通递归时,内存需要记录调用的堆栈所出的深度和位置信息。在最低层计算返回值,再根据记录的信息,跳会上一层级计算,然后再跳回到更高一层,依次运行,直到最外层的调用函数。在cpu计算和内存会消耗很多,而且当深度过大时,会出现堆栈溢出
function sum(x){
if(x===1) return 1;
return x+sum(x-1);
}
sum(5); //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 //尾递归,每次执行之后,函数重新传入参数,直到结束
整个计算过程是线性的,调用一次sum(x,total)后,会进入下一个栈,相关的数据信息和跟随进入,不再放在堆栈上保存。当计算完最后的值之后,直接返回到最上层的sum(5,0).这能有效的防止堆栈溢出。
在ECMAScript6,我们将迎来尾递归优化,通过尾递归优化,javascript代码在解释成机器码的时候,将会向while看起,也就是说,同时拥有数学表达能力和while的效能。
闭包
自己领会
函数式编程比较火热的库
- Rxjs //截流与仿抖
- cyclejs
- lodashjs
- underscorejs //开始学最佳的库
- ramadajs
需要学习
范畴与容器
- 我们可以把“范畴”想象成是一个容器,里面包含两样东西。值(value)、值的变形关系,也就是函数。
- 范畴论使用函数,表达范畴之间的关系。
- 伴随着范畴论的发展,就发展出一整套函数的运算方法。这套方法起初只用于数学运算,后来有人将它在计算机上实现了,就变成了今天的“函数式编程”。
- 本质上,函数式编程只是范畴论的运算方法,跟数理逻辑、微积分、行列式式同一类东西,都是数学方法,只是碰巧他能用来写程序。为什么函数式编程要求函数必须是纯的,不能有副作用?因为它是一种数学运算,原始目的就是求值,不做其他事情,否则就无法满足函数运算法则了。
函子是函数式编程里面最重要的数据类型,也是基本的运算单位和功能单位。它首先是一种范畴,也就是说,是一个容器,包含了值和变形关系。比较特殊的是,它的变形关系可以依次作用于每一个值,将当前容器变形成另一个容器。
容器、Functor(函子)
- $(...)返回的对象并不是一个原生的DOM对象,而是对于原生对象的一种封装,这在某种意义上就是一个"容器"(但它并不函数式)
- Functor(函子)遵守一些特定规则的容器类型
- 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')
Maybe 函子
函子接受各种函数,处理容器内部的值,这里就有一个问题,容器内部的值可能是一个空值(比如null),而外部函数未必有处理空值的机制,如果传入空值,很可能就会出错。
Functor.of(null).map(function(s){
return s.toUpperCase();
});
//TypeError
class Maybe extends Functor{
map(f){
return this.val?Maybe.of(f(this.val)):Maybe.of(null);
}
}
Maybe.of(null).map(function(s){
return s.toUpperCase();
});
//Maybe(null) //报错,未定义
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(null) //不会报错了
//新的容器我们称为Maybe
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);
}
var addOne=function(x){
return x+1;
}
Either.of(5,6).map(addOne); //Either(5,7);
Either.of(1,null).map(addOne); //Either(2);
Either
//右值中有address这个属性,则覆盖原来的xxx,否则使用默认的xxx
.of({address:'xxx'},currentUser.address) .map(updateField);
es5写法
错误处理、Either
var Left=function(x){
this.__value=x;
}
var Rigth=function(x){
this.__value=x;
}
Left.of=function(x){
return new Left(x);
}
Right.of=function(x){
return new Right(x);
}
Left.prototype.map=functin(f){
return this;
}
Right.prototype.map=function(f){
return Right.of(f(this.__value));
}
Left和Right唯一的区别就在于map方法的实现,Right.map的行为和我们之前提到的map函数一样。但是Left.map就很不同了:它不会对容器做任何事情,只是很简单地把这个容器拿进来又扔出去。这个特性意味着,Left可以用来传递一个错误消息。
var getAge = user => user.age ? Right.of(user.age):Left.of("Error");;
getAge({name:'stark',age:'21'}).map(age=>'Age is '+age);
//Right('Age is 21');
getAge({name:'stark'}).map({age=>'Age is '+age});
//Left('Error');
Left 可以让调用链中任意一环的错误立即返回到调用链的尾部,这给我们错误处理带来了很大的方便,再也不用一层又一层的Try/catch
AP因子
函子里面包含的值,完全可能是函数。我们可以想象这样一种情况,一个函子的值是数值,另一个函子的值是函子。
class Ap extends Functor{
ap(F){
return Ap.of(this.val(F.val));
}
}
Ap.of(addTwo).ap(Functor.of(2));
实例
function Functor(val){
this.__val=val;
}
Functor.of=function(val){
return new Functor(val);
}
Functor.prototype.map=function(fn){
return Functor.of(fn(this.__val));
}
function addTwo(x){
return x+2;
}
function Ap(val){
Functor.call(this,val);
}
Ap.of=function(val){
return new Ap(val);
}
var __proto=Object.create(Functor.prototype);
__proto.constructor=Ap.prototype.constructor;
Ap.prototype=__proto;
Ap.prototype.ap=function(F){
return Ap.of(this.__val(F.__val));
}
const A=Functor.of(2);
const B=Ap.of(addTwo);
console.log(B.ap(A)); //4
//console.log(B.ap(A).ap(A));此时会报错,B.ap(A)其中A不是个函数
IO
真正的程序总要去接触肮脏的世界
function readLoaclStorage(){
return window.localStorage;
}
Io跟前面那几个Functor不同的地方在于,他的__value是一个函数。它把不纯的操作(比如IO、网络请求、DOM)包裹到一个函数内,从而延迟这个操作的执行。所以我们认为,IO包含的是被包裹的操作的返回值。
IO其实也算是惰性求值
IO负责了调用链积累了很多很多不纯的操作,带来的复杂性和不可维护性。
import _ from 'lodash';
var compose=_.flowRight;
var IO=function(f){
this._value=f;
}
IO.of=x=>new IO(_=>x);
IO.prototype.map=function(f){
return new IO(compose(f,this.__value));
}
Monad
Monad就是一种设计模式,表示将一个运算过程,通过函数拆解成互相连接的多个步骤。你只要提供下一步运算所需的函数,整个运算就会自动进行下去。
Promise就是一种Monad
Monad糖我们避开了嵌套地狱,可以轻松地进行深度嵌套的函数式编程,比如IO和其它异步任务
Maybe.of(
Maybe.of(
Maybe.of({name:'Mulburry',number:99})
)
)
class Monad extends Functor{
join(){
return this.val;
}
flatMap(f){
return this.map(f).join();
}
}
Monad 函子的作用是,总是返回一个单层的函子。它有一个flatMap方法,与map方法作用相同,唯一的区别是如果生成了一个嵌套函子,他会取出后这内部的值,保证返回的永远是一个单层的容器,不会出现嵌套的情况。
如果函数f返回的是一个函子,那么this.map(f)就会生成一个嵌套的函子。所以,join方法保证了flatMap方法总是返回一个单层的函子。这意味着嵌套的汉子会被铺平(flatMap)