下一篇《函数式编程之Promise的奇幻漂流》 javascript
曾经的你是否是总在工做和学习过程当中听到函数式编程(FP)。但学到函子的时候老是一头雾水。本文是我在函数式编程学习过程当中,总结的笔记,也分享给想学函数式编程的同窗。html
在学以前,你先问本身几个问题,或者看成一场面试,看看下面的这些问题,你该怎么回答?前端
var Container = function(x) { this.__value = x; }
Container.of = x => new Container(x);
Container.prototype.map = function(f){
console.log(f)
return Container.of(f(this.__value))
}
Container.of(3).map(x=>x+1).map(x => 'Result is ' + x);
console.log(Container.of(3).map(x=>x+1).map(x => 'Result is ' + x))
复制代码
如今就让咱们带着问题去学习吧。文章的最后,咱们再次总结这些问题的答案。java
面对对象(OOP)能够理解为是对数据的抽象,好比把一我的抽象成一个Object,关注的是数据。 函数式编程是一种过程抽象的思惟,就是对当前的动做去进行抽象,关注的是动做。react
举个例子:若是一个数a=1 ,咱们但愿执行+3(f函数),而后再*5(g函数),最后获得结果result是20
数据抽象,咱们关注的是这个数据:a=1 通过f处理获得 a=4 , 再通过g处理获得 a = 20
过程抽象,咱们关注的是过程:a要执行两个f,g两操做,先将fg合并成一个K操做,而后a直接执行K,获得 a=20
复制代码
问题:f和g合并成了K,那么能够合并的函数须要符合什么条件呢?下面就讲到了纯函数的这个概念。es6
定义:一个函数若是输入参数肯定,输出结果是惟一肯定的,那么他就是纯函数。
特色:无状态,无反作用,无关时序,幂等(不管调用多少次,结果相同)面试
下面哪些是纯函数 ?编程
let arr = [1,2,3];
arr.slice(0,3); //是纯函数
arr.splice(0,3); //不是纯函数,对外有影响
function add(x,y){ // 是纯函数
return x + y // 无状态,无反作用,无关时序,幂等
} // 输入参数肯定,输出结果是惟一肯定
let count = 0; //不是纯函数
function addCount(){ //输出不肯定
count++ // 有反作用
}
function random(min,max){ // 不是纯函数
return Math.floor(Math.radom() * ( max - min)) + min // 输出不肯定
} // 但注意它没有反作用
function setColor(el,color){ //不是纯函数
el.style.color = color ; //直接操做了DOM,对外有反作用
}
复制代码
是否是很简单,接下来咱们加一个需求?
若是最后一个函数,你但愿批量去操做一组li而且还有许多这样的需求要改,写一个公共函数?数组
function change (fn , els , color){
Array.from(els).map((item)=>(fn(item,color)))
}
change(setColor,oLi,"blue")
复制代码
那么问题来了这个函数是纯函数吗?bash
首先不管输入什么,输出都是undefined,接下来咱们分析一下对外面有没有影响,咱们发现,在函数里并无直接的影响,可是调用的setColor对外面产生了影响。那么change到底算不算纯函数呢?
答案是固然不算,这里咱们强调一点,纯函数的依赖必须是无影响的,也就是说,在内部引用的函数也不能对外形成影响。
问题:那么咱们有没有什么办法,把这个函数提纯呢?
定义:只传递给函数一部分参数来调用它,让它返回一个函数去处理剩下的参数。
javascript
function add(x, y) {
return x + y;
}
add(1, 2)
******* 柯里化以后 *************
function addX(y) {
return function (x) {
return x + y;
};
}
var newAdd = addX(2)
newAdd (1)
复制代码
如今咱们回过头来看上一节的问题?
若是咱们不让setColor在change函数里去执行,那么change不就是纯函数了吗?
javascript
function change (fn , els , color){
Array.from(els).map((item)=>(fn(item,color)))
}
change(setColor,oLi,"blue")
****** 柯里化以后 *************
function change(fn){
return function(els,color){
Array.from(els).map((item)=>(fn(item,color)))
}
}
var newSetColor = change(setColor);
newSetColor(oLi,"blue")
复制代码
上面那个例子是直接重写了change函数,能不能直接在原来change的基础上经过一个函数改为 newSetColor呢?
javascript
function change (fn , els , color){
Array.from(els).map((item)=>(fn(item,color)))
}
change(setColor,oLi,"blue")
//******* 经过一个curry函数*************
var changeCurry = curry(change);
var newSetColor = changeCurry(setColor);
newSetColor(oLi,"blue")
复制代码
哇!真的有这种函数吗?固然做为帮助函数(helper function),lodash 或 ramda都有啊。咱们在深刻的系列的课程中会动(chao)手(xi)写一个。
问题:处理上一个问题时,咱们将一个函数做为参数传到另外一个函数中去处理,这好像在函数式编程中很常见,他们有什么规律吗?
定义:函数当参数,把传入的函数作一个封装,而后返回这个封装函数,达到更高程度的抽象。
很显然上一节用传入fn的change函数就是一个高阶函数,显然它是一个纯函数,对外没有反作用。可能这么讲并不能让你真正去理解高阶函数,那么我就举几个例子!
定义 :调用函数自己的地方均可以其等价函数;
javascript
function __equal__(fn){
return function(...args){
return fn.apply(this,args);
}
}
//第一种
function add(x,y){
return x + y
}
var addnew1 = __equal__(add);
console.log(add(1,2));
console.log(addnew1(1,2));
//第二种
let obj = {
x : 1,
y : 2,
add : function (){
console.log(this)
return this.x + this.y
}
}
var addnew2 = __equal__(obj.add);
console.log( obj.add() ) ; //3
console.log( addnew2.call(obj)); //3
复制代码
第一种不考虑this
第二种考虑this
好了,看懂代码后,咱们发现,这好像和直接把函数赋值给一个变量没啥区别,那么等价函数有什么好处呢?
等价函数的拦截和监控:
javascript
function __watch__(fn){
//偷偷干点啥
return function(...args){
//偷偷干点啥
let ret = fn.apply(this,args);
//偷偷干点啥
return ret
}
}
复制代码
咱们知道,上面本质就是等价函数,fn执行结果没有任务问题。可是能够在执行先后,偷偷作点事情,好比consle.log("我执行啦")。
问题:等价函数能够用于拦截和监控,那有什么具体的例子吗?
前端开发中会遇到一些频繁的事件触发,为了解决这个问题,通常有两种解决方案:
javascript
function throttle(fn,wait){
var timer;
return function(...args){
if(!timer){
timer = setTimeout(()=>timer=null , wait);
console.log(timer)
return fn.apply(this,args)
}
}
}
const fn = function(){ console.log("btn clicked")}
const btn = document.getElementById('btn');
btn.onclick = throttle(fn , 5000);
复制代码
分析代码
因此,咱们经过对等价函数监控和拦截很好的实现了节流(throtle)函数。而对函数fn执行的结果丝毫没有影响。这里给你们留一个做业,既然咱们实现了节流函数,那么你能不能根据一样的原理写出防抖函数呢?
问题:哦,像这样节流函数,在我平时的项目中直接写就行了,你封装成这样一个函数彷佛还麻烦了呢?
在平时,若是咱们不借助方法函数去实现节流函数,咱们可能会直接这么去实现节流函数。
var timer;
btn.onclick = function(){
if(!timer){
timer = setTimeout(()=>timer=null , 5000);
console.log("btn clicked")
}
}
复制代码
那么与以前的高阶函数有什么区别呢?
很显然,在下面的这例子中,咱们每次在须要作节流的时候,咱们每次都须要这样从新写一次代码。告诉 程序如何执行。而上面的高阶函数的例子,咱们定义好了一个功能函数以后,咱们只须要告诉程序,你要作 什么就能够啦。
那下面你们看看,若是遍历一个数组,打印出每一个数组中的元素,如何用两种方法实现呢?
//命令式
var array = [1,2,3];
for (i=0; i<array.length;i++){
console.log(array[i])
}
//声明式
array.forEach((i) => console.log(i))
复制代码
看到forEach是否是很熟悉,原来咱们早就在大量使用函数式编程啦。
这里咱们能够先停下来从头回顾一下,函数式编程。
问题:如今咱们对函数编程有了初步的了解,但还并无感觉到它的厉害,还记得咱们以前讲到的纯函数能够合并吗?下一节,咱们就去实现它
function double(x) {
return x * 2
}
function add5(x) {
return x + 5
}
double(add5(1))
复制代码
上面的代码咱们实现的是完成了两个动做,不过咱们以为这样写double(add5(x)),不是很舒服。 换一个角度思考,咱们是否是能够把函数合并在一块儿。 咱们定义了一个compose函数
var compose = function(f, g) {
return function(x) {
return f(g(x));
};
};
复制代码
有了compose这个函数,显然咱们能够把double和add5合并到一块儿
var numDeal = compose(double,add5)
numDeal(1)
复制代码
那么这时候就有几个问题,
这三道题咱们留做思考题。咱们在深刻的专题里会去实现的哈。
问题:如今咱们想完成一些功能都须要去合并函数,并且合并的函数还会有必定顺序,咱们能不能像JQ的链式调用那样去处理数据呢。
讲到函子,咱们首先回到咱们的问题上来。以前咱们执行函数一般是下面这样。
function double(x) {
return x * 2
}
function add5(x) {
return x + 5
}
double(add5(1))
//或者
var a = add5(5)
double(a)
复制代码
那如今咱们想以数据为核心,一个动做一个动做去执行。
(5).add5().double()
复制代码
显然,若是能这样执行函数的话,就舒服多啦。那么咱们知道,这样的去调用要知足
因此咱们试着去给他建立一个引用类型
class Num{
constructor (value) {
this.value = value ;
}
add5(){
return this.value + 5
}
double(){
return this.value * 2
}
}
var num = new Num(5);
num.add5()
复制代码
咱们发现这个时候有一个问题,就是咱们通过调用后,返回的就是一个值了,咱们没有办法进行下一步处理。因此咱们须要返回一个对象。
class Num{
constructor (value) {
this.value = value ;
}
add5 () {
return new Num( this.value + 5)
}
double () {
return new Num( this.value * 2)
}
}
var num = new Num(2);
num.add5 ().double ()
复制代码
咱们发现,new Num( this.value + 5),中对this.value的处理,彻底能够经过传进去一个函数去处理
而且在真实状况中,咱们也不可能为每一个实例都建立这样有不一样方法的构造函数,它们须要一个统一的方法。
class Num{
constructor (value) {
this.value = value ;
}
map (fn) {
return new Num(fn(this.value))
}
}
var num = new Num(2);
num.map(add5).map(double)
复制代码
咱们建立了一个map的方法,把处理的函数fn传了进去。这样咱们就完美的实现啦,咱们设想的功能啦。
最后咱们整理一下,这个函数。
class Functor{
constructor (value) {
this.value = value ;
}
map (fn) {
return Functor.of(fn(this.value))
}
}
Functor.of = function (val) {
return new Functor(val);
}
Functor.of(5).map(add5).map(double)
复制代码
如今Functor.of(5).map(add5).map(double)去调用函数。有没有以为很爽。
哈哈,更爽的是,你已经在不知不觉间把函子的概念学完啦。上面这个例子总的Functor就是函子。如今咱们来总结一下,它有那些特色吧。
嗯,这下明白什么是函子了吧。在初学函数编程时,必定不要太过于纠结概念。看到好多,教程上在讲 函子时全然不提JavaScript语法。用生硬的数学概念去解释。
我我的以为书读百遍,其义自见。对于编程范式的概念理解也是同样的,你先知道它是什么。怎么用。 多写多练,天然就理解其中的含义啦。总抱着一堆概念看,是很难看懂的。
以上,函子(Functor)的解释过程,我的理解。也欢迎你们指正。
问题:咱们实现了一个最通用的函子,如今别问问题,咱们趁热打铁,再学一个函子
咱们知道,在作字符串处理的时候,若是一个字符串是null, 那么对它进行toUpperCase(); 就会报错。
Functor.of(null).map(function (s) {
return s.toUpperCase();
});
复制代码
那么咱们在Functor函子上去进行调用,一样也会报错。
那么咱们有没有什么办法在函子里把空值过滤掉呢。
class Maybe{
constructor (value) {
this.value = value ;
}
map (fn) {
return this.value ? Maybe.of(fn(this.value)) : Maybe.of(null);
}
}
Maybe.of = function (val) {
return new Maybe(val);
}
var a = Maybe.of(null).map(function (s) {
return s.toUpperCase();
});
复制代码
咱们看到只须要把在中设置一个空值过滤,就能够完成这样一个Maybe函子。
因此各类不一样类型的函子,会完成不一样的功能。学到这,咱们发现,每一个函子并无直接去操做须要处理的数据,也没有参与处处理数据的函数中来。
而是在这中间作了一些拦截和过滤。这和咱们的高阶函数是否是有点像呢。因此你如今对函数式编程是否是有了更深的了解啦。
如今咱们就用函数式编程作一个小练习: 咱们有一个字符串‘li’,咱们但愿处理成大写的字符串,而后加载到id为text的div上
var str = 'li';
Maybe.of(str).map(toUpperCase).map(html('text'))
复制代码
若是在有编写好的Maybe函子和两个功能函数的时候,咱们只须要一行代码就能够搞定啦
那么下面看看,咱们的依赖函数吧。
let $$ = id => Maybe.of(document.getElementById(id));
class Maybe{
constructor(value){
this.__value = value;
}
map(fn){
return this.__value ? Maybe.of(fn(this.__value)) : Maybe.of(null);
}
static of(value){
return new Maybe(value);
}
}
let toUpperCase = str => str.toUpperCase();
let html = id => html => {
$$(id).map(dom => {
dom.innerHTML = html;
});
};
复制代码
咱们来分析一下代码
var html = function(id) {
return function (html) {
$$(id).map(function (dom) {
dom.innerHTML = html;
});
};
};
复制代码
你们再来想一个问题 Maybe.of(str).map(toUpperCase).map(html('text'))最后的值是什么呢?
咱们发现最后没有处理的函数没有返回值,因此最后结果应该是 Maybe {__value: undefined}; 这里面给你们留一个问题,咱们把字符串打印在div上以后想继续操做字符串该怎么办呢?
问题:在理解了函子这个概念以后,咱们来学习本文最后一节内容。有没有很开心
Monad函子也是一个函子,其实很原理简单,只不过它的功能比较重要。那咱们来看看它与其它的 有什么不一样吧。
咱们先来看这样一个例子,手敲在控制台打印一下。
var a = Maybe.of( Maybe.of( Maybe.of('str') ) )
console.log(a);
console.log(a.map(fn));
console.log(a.map(fn).map(fn));
function fn(e){ return e.value }
复制代码
class Maybe{
constructor (value) {
this.value = value ;
}
map (fn) {
return this.value ? Maybe.of(fn(this.value)) : Maybe.of(null);
}
join ( ) {
return this.value;
}
}
Maybe.of = function (val) {
return new Maybe(val);
}
复制代码
咱们想取到里面的值,就把它用join方法返回来就行了啊。因此我给它加了一个join方法
var a = Maybe.of( Maybe.of('str') )
console.log(a.join().map(toUpperCase))
复制代码
因此如今咱们能够经过,join的方法一层一层获得里面的数据,并把它处理成大写
如今你确定会好奇为何会产生Maybe.of( Maybe.of('str')) 结构呢?
还记得html那个函数吗?咱们以前留了一个问题,字符串打印在div上以后想继续操做字符串该怎么办呢?
很显然咱们须要让这个函数有返回值。
let html = id => html => {
return $$(id).map(dom => {
dom.innerHTML = html;
return html
});
};
复制代码
分析一下代码。
那么这时候咱们想,既然咱们在执行的时候就知道,它会有影响,那我能不能在执行的时候,就把这个应该 给消除呢。
class Maybe{
constructor (value) {
this.value = value ;
}
map (fn) {
return this.value ? Maybe.of(fn(this.value)) : Maybe.of(null);
}
join ( ){
return this.value;
}
chain(fn) {
return this.map(fn).join();
}
}
复制代码
咱们写了一个chain函数。首先它调用了一下map方法,执行结束后,在去掉一层嵌套的函子
因此在执行的时候,咱们就能够这样去写。
Maybe.of(str).map(toUpperCase).chain(html('text'))
复制代码
这样返回的函数就是只有一层嵌套的函子啦。
学到这里咱们已经把所有的函数式编程所涉及到概念都学习完啦。如今要是面试官拿这样一道题问题,答案是什么?是否是有点太简单啦。
var Container = function(x) { this.__value = x; }
Container.of = x => new Container(x);
Container.prototype.map = function(f){
console.log(f)
return Container.of(f(this.__value))
}
Container.of(3).map(x=>x+1).map(x => 'Result is ' + x);
console.log(Container.of(3).map(x=>x+1).map(x => 'Result is ' + x))
复制代码
但你会发现咱们并无具体纠结每个概念上,而是更多的体如今可实现的代码上,而这些代码你也并不陌生。
哈哈,那你可能会问,我是否是学了假的函数式编程,并无。由于我以为函数式编程也是编程,最终都是要回归到平常项目的实践中。而应对不一样难度的项目,所运用的知识固然也是不同的,就比如造船,小船有小船的造法,邮轮有油轮的造法,航母有航母的造法。你没有 必要把所有的造船知识点,逐一学完才开始动手。平常何况在工做中,你可能也并有真正的机会去造航母(好比写框架)。与其把大量的时间都花在理解那些概念上,不如先动手造一艘小船踏实。因此本文中大量淡化了不须要去当即学习的概念。
如今,当你置身在函数式编程的那片海中,看见泛起的一叶叶扁舟,是否是再也不陌生了呢?
是否是在海角和天边,还划出一道美丽的曲线?
那么接下来咱们会动手实践一个Underscore.js 的库。进一步深刻每一个细节去了解函数式编程。 学习更多的技巧。
最后本文是我学习函数式编程的笔记,写的时候常常自言自语,偶尔还安慰本身。若是有错的地方,欢迎你们批评指正。
文章最后总结的上面的答案是有的,不过如今还在我心中,等我有时间在写啊 啊 啊。。。。