函数式编程

本文来自我的github转载时请注明出处

0.前言

本文并不是完全按照严格意义的函数式编程来讲,主要是抽取一些思想和js结合,以达到写出更有水平的代码的目的。

1.概念

1.1介绍

在计算机科学里,函数式编程是一种编程范式,它将计算描述为表达式求值并避免了状态和数据改变。
函数式编程(FP)思想像数学计算,比如数学中有一个函数为f(x)=x+1,那么f(0)=1,输出结果只和输入有关。函数可以看作是一个管道,一边进去另外一边输出结果(自变量进去,因变量出来)。数学函数的特点是对于每一个自变量,存在唯一的因变量与之对应,形成一一对应的映射关系(数学函数的单值性)。只要编程函数是纯函数,编程函数就可像数学的函数一样进行函数式编程。

function add(a,b){
return a+b
}

好处?
- 语义清晰
- 复用性高
- 维护性好
- 副作用少

副作用?(除了返回值之外,它可以与外部条件进行交互。如new Date())
数学中的函数,如果是一个值需要经过多个函数才能得到结果:f(’test’)=’a’,g(’a’)=’result’,那么可以写作一个复合函数的形式:componse(‘test’) = g(f(’test’))=’result’
同样的,写作js程序就是:

function f(x){
if(x=='test')  return 'a'
}
function g(x){
if(x=='a')return 'result'
}
function componse(x){
return g(f(x))
}
componse('test')

componse是复合函数,输入如果是’test’,输出结果就是’result’

1.2纯函数的条件

  • 每个函数必须有输入参数(类似数学函数自变量)
  • 每个函数必须有返回值(类似因变量)
  • 同一个参数调用函数时,返回值一致。
  • 返回值仅由其输入值决定,并且不产生副作用

这样子,就可以“看起来好像数学的函数”,进行函数式编程了

const greet = (name) => \`Hello, ${name}\`//纯函数
greet('world')

const greet = () => ‘Hello, world’//不是纯函数
greet()
greet(‘hi’)

2.高阶函数

以函数为参数,返回另一个函数。
比如上面所说的componse函数,改为函数式编程的话,就是

var componse = (x)=>(x=='test'?'a':undefined)=='a'?'result':undefined

以函数f的输出作为g的输入

再举一个在一个大的函数里面,包含函数处理参数的例子:
过滤一个数组,使得数组只有字符串:

[0, '1', null].filter(function(x){
        return Object(x) instanceof String
})

把参数、过滤函数都作为一个大的函数的参数,写作函数式编程,就可以这样:

var filter = (predicate, arr) => arr.filter(predicate)
var isStr = (x) => Object(x) instanceof String
filter(isStr, [0, '1', null]) //'1'

不仅如此,部分还可以带上的初始值。这是一个这样的函数,f是函数,arg是f接受的原始值参数集合,otherArgs是后面传进来的参数集合,最后返回的结果是原始值和后面传进来的值作为f的参数,返回结果:

var fp = function(f,...args){
    return function(...otherArgs){
        return f(...args,...otherArgs)
    }
 }
fp((x,y,z)=>x+y+z,2)(1,120)//123,2 是原始值
fp((x,y,z)=>x+y+z,1,2)(1)//4   ,1和2是原始值
fp((x,y,z)=>x+y+z,2,3)(1,120)//6,因为只接受3个参数,最后的120没有参与计算

2.1数组的方法

通过高阶函数,数组中的每一个元素可以被应用到一个函数上,并返回新的数组。
比如,一个数组的排序函数,两个参数:数组和排序函数,我们就可以写作:

var arrsort = function(f,arr){
    return arr.sort(f)
}
//es6:
var arrsort = (f,arr)=>arr.sort(f)
arrsort((a,b)=>a-b,[1,5,4,3])//[1,3,4,5]

尝试用reduce实现一个Array.map:

function myMap(f, arr) {
  return arr.reduce(function(result, x) {
    result.push(f(x));
    return result;
  }, []);
}
myMap((x)=>x+1,[1,2,3,4])//[2,3,4,5]

2.3闭包

闭包的现象:函数中的函数、外作用域可以访问内作用域的变量,闭包的结果:内存泄露(泄漏一点点内存没所谓)。中间缓存的变量,其实也是一种闭包的结果。代码块执行完成后,函数 add1 的状态也被保存

var add = x => y => x + y
var add1 = add(1) //缓存x=1,return的是1+y
add1(3) // 4

回头看前面的例子,

var filter = (predicate, arr) => arr.filter(predicate)
var isStr = (x) => Object(x) instanceof String
filter(isStr, [0, '1', null]) //'1'

前面的缓存var isStr = (x) => Object(x) instanceof String这个函数,也是通过闭包,使得在这个作用域能够捕获访问函数的局部变量,即使执行已经从定义它的块中移出。它们允许在声明变量的块执行完成之后保持对作用域的引用,不会被垃圾回收

2.4链式调用

我们计算1+2+3+4时候,如果用var add = (a,b)=>a+b这个函数,则是add(add(add(1, 2), 3), 4),这样子显然可读性和维护性很差,所以我们可以优化一下:

var _ = {
    sum:0,
  add(x) {  
    this.sum += x;  
    return this;  
 } 
};  
_.add(1).add(2).add(3).add(4).sum 

是不是发现有点像jQuery和promise呢?
jQuery实例对象,执行完他的方法后返回的还是jQuery实例对象,所以还可以继续执行jQuery实例的方法。promise解决了回调地狱的问题,promise的then也是一样的道理,返回的是promise对象,所以可以继续使用他本身的方法(比如then)
于是,我们马上来自己造一个promise看看:

function MyPromise(fn){
                var count = 0;
                var that = this
                this.list = [];//存放后面的then
                this.then = function(callback){
                    this.list.push(callback);
                    return this;
                }
                this.oresolve = function(){//resolve一个就算一个,直到then运行完
                    if(count == that.list.length) {
                        return;
                    }
                    that.list\[count++\](that.oresolve);
                }
                setTimeout(()=>{//模仿异步编程
                    fn(that.oresolve);
                },0);
            }
//试一下:
function a(oresolve){
                setTimeout(function(){
                    console.log("1");
                    oresolve();
                }, 1000);
            }           
            function b(oresolve){
                setTimeout(function(){
                    console.log("2");
                    oresolve();
                }, 1000);
            }
new MyPromise(a).then(b)//1、2

虽然这不是严格意义的函数式编程,但是这种链式调用是一种类似的思想,像一条管道一样,一边进一边出,而且代码易于维护

3.柯里化

将多元函数转换为 一元函数的过程。因为闭包的存在,可以使得柯里化更加方便。
比如sum = (a, b) => a + b柯里化后sum = (a) => (b) => a + b,柯里化后sum(1)(2)==3,如果设置一个中间变量sum1=sum(1)(保持对sum(1)的结果的引用),那么sum1(3)==4,有一种电子计算机的感觉吧,按下1+1,然后输出个2,再按+1输出3

3.1手动给函数加上柯里化的能力

直接在函数的原型对象上加上一个curry的方法

Function.prototype.curry = function() {
       var defaultArgs = [...arguments];
        var that = this;
        return function() {
            return that.apply(this,defaultArgs.concat([...arguments]));    
            }
        }

于是我们就可以用一下试试看

 function add(a, b) {//定义一个加法函数测试一下
            return a + b;
        }
        var add1 = add.curry(1);//实际上是1+b
        console.log(add1(5)); // 6
        console.log(add1(2)); // 3
        var add2= add.curry(1,2)
        add2() //3
        add2(1)//3,参数是两个,第三个参数不参与

3.2自动柯里化

lodash 里面有一个curry函数 _.curry(f),如果给定的参数数量少于需要的参数,则返回一个函数,该函数将获得其余的参数。当函数得到足够参数时,就会有返回值。
我们已经自己写出了一个function原型上的curry方法,那也可以写出类似效果的自动柯里化,比如上面的就可以有这样的效果:add.curry(1,2)()//3,add.curry(1)(2)//3
自动柯里化后的结果:

var curryAdd = function (){
    return arguments.length<add.length?add.curry(...arguments):add(...arguments)
}
curryAdd(1)//一个函数add.curry(1)
curryAdd(1)(2)//3
curryAdd(1,2)//3
curryAdd(1,2,3)//3

4.从代理模式到函数记忆

4.1 代理模式

是一个对象找一个替代对象,以便对原对象进行访问,在我们不愿意或者不想对原对象进行直接操作时候,我们可以使用代理,让它帮原对象进行一系列的操作。

var Dinner = function(name){//定义晚餐类
   this.name = name;
};
Dinner.prototype.getName = function() {//晚餐类被取名字的方法
   return this.name;
};
// 定义一个代理
var proxy = {
   cookDinner: function(dinner) {
       boss.cookDinner(dinner.getName())
   }
};
// 定义一个boss对象
var boss = {
   cookDinner: function(name) {
       console.log('已经做好了晚餐:' + name);
   }
};
proxy.cookDinner(new Dinner('黄焖鸡米饭')); //已经做好了晚餐:黄焖鸡米饭

这里boss会做饭,代理也会做饭,但是boss并不希望自己做饭,所以交给代理做饭。

4.2 缓存代理

缓存代理:函数第一次遇到某个参数,并算出了返回值,之后如果遇到相同参数就直接返回缓存结果,无需执行计算过程。

var add = (a,b)=>a+b
var proxyAdd = (function() {
   var cache = {};//缓存
   return function() {
       var args = Array.prototype.join.call(arguments, ',');
       if(args in cache) {
           return cache[args];//直接返回缓存结果
       }
       return caches[args] = add.apply(this, arguments);//将第一次遇到的k-v组合缓存
   }
})();
console.time('第一次')
proxyAdd(1, 2); 
console.timeEnd('第一次')
console.time('第2次')
proxyAdd(1, 2); 
console.timeEnd('第2次')

测试了几次:(复制函数定义部分+console部分粘贴运行)
- 第一次: 1.021ms
第2次: 0.074ms
- 第一次: 0.376ms
第2次: 0.060ms
- 第一次: 0.180ms
第2次: 0.041ms
显然第二次是比较快的
※如果是已经定义了函数,多次运行console部分,时间就是不稳定的(就像平时js在浏览器中的运行时间也是不稳定的)

4.3函数记忆

类似于缓存代理,我们先定义一个记忆函数

var memorize = function(f,hasher){
        //用闭包缓存
        var mem = function(name){
            var cache = mem.cache
            var key = "" + (hasher?hasher.apply(this.arguments):name)
            if (!cache[key]) {
                cache[key] = f.apply(this,arguments)
            }
            return cache[key]
        }
        mem.cache = {}
        return mem
    }
var add = (a,b)=>a+b;
var memorizedAdd = memorize(add,function(){
        var args = Array.prototype.slice.call(arguments)
        return JSON.stringify(args)
    })
memorizedAdd(1,2)//3
memorizedAdd(1,2)//第二次不用计算就可以知道结果

有的人就说了,console.time居然慢了,其实对于简单计算,并不需要函数记忆,用了反而更加慢。
可是对于递归的经典问题斐波那契数列,他的作用就来了

//常规的斐波那契数列递归
var count = 0
    var fob = function(n){
        count ++
        return n<2?n:fob(n-1) + fob(n-2)
    }
    fob(10)
    console.log('执行了'+count+'次')//177

但是,如果用到了函数记忆,运行次数将会大大减小

var count = 0
    var fob = function(n){
        count ++
        return n<2?n:fob(n-1) + fob(n-2)
    }
    var fob = memorize(fob)//用上函数记忆
    fob(10)
    console.log('执行了'+count+'次')//11

常规的递归,次数多是因为,第一个数+第二个数=第三个数,第四个数=第三个数+第二个数=第一个数+第二个数+第二个数……一直下去,而我们已知的就是前面两个数,如果没有缓存,他们将会使劲往下找,直到找到前面两个数。如果用上函数记忆,他们每一次运行后的结果都会被记住,而不是往下递归寻找前面两个数了。

5.从函数记忆到递归

5.1递归

一般,我们要想在控制台打印1-10,都会用循环,循环i从0到10:

for(var i = 0;i<10;i++){
console.log(i)
}

在函数式编程中,我们就少用循环,以递归来代替:

(function f(i){
    if (i>10) {
        return 
    }
    console.log(i)
    f(i+1)
})(0)
//简写:
(f=(i)=>{
    console.log(i++)
    return i>10?undefined:f(i)
})(0)

我们知道,函数调用会在内存形成一个调用帧,保存调用位置和内部变量等信息。如果在函数A的内部调用函数B,那么在A的调用记录上方,还会形成一个B的调用记录。等到B运行结束,将结果返回到A,B的调用记录才会消失。如果函数B内部还调用函数C,那就还有一个C的调用记录栈,以此类推。所有的调用记录,就形成一个调用栈。所以,普通的递归容易导致栈的溢出,而且效率不如普通的循环,所以需要尾递归进行优化。

5.2尾递归

尾调用,就是某个函数的最后一步是调用另一个函数。那么显然,尾递归就是调用的那个函数就是自己。
由于是函数的最后一步,所以不需要保留外层函数的调用记录,因为调用位置、内部变量等信息都不会再用到了,只要直接用内层函数的调用记录,取代外层函数的调用记录就可以了。
如果所有函数都是尾调用,那么完全可以做到每次执行时,调用记录只有一项,这将大大节省内存。这就是尾递归的意义。
对比:
正常递归(非尾递归,尾部调用非自身)

function f(n){ 
return g(f(n-1)) 
} 

用数学公式的角度就是f(x)=g(f(x-1))
假设函数内部的关系用g表示(加减乘除幂对数赋值等等),对于函数f的n次递归,正常的递归就是f(n)=g(f(n-1))=g(g(f(n-2)))=…=g(g(g(…..g(f(1))))),里面要嵌套n个g,复杂度O(n)
尾递归(尾部调用自身,没毛病,return就是f自己)

function f(n){ 
return f(n-1) 
} 
f(n)=f(n-1)=…=f(1

复杂度就是O(1)

那么对于阶乘递归问题,一般的递归:

var s = 1
var mult = (x)=>{
    s*=x--
    if(x==1){
        return s
    }
    return mult(x)
}

进行优化后:var mult = (n,s)=>n===1?s:mult(n-1,n*s)

对于常规的递归的斐波那契var fob=(n)=>n<=1?n: fob(n-1)+fob(n-2),如果经过了尾递归,则可以写作:
var fob = (n)=>(f=(a,b,s)=>s

6.真-函数式编程

严格意义上的函数式编程,和上面讲的还是有区别的。我这里为了方便理解,就没有太过于遵守这个规则,实际上为了追求这种思想而不是完全照搬,来增加代码的简洁性。
- 用const定义,即是无变量。
- 条件表达式condition ? expr1 : expr2代替if-else
- 禁用循环,用递归代替
- 不能使用prototype和this,完全脱离JS中的面向对象编程。
- 不要使用with语句。
- 用===,避免==的自动类型转换
….
还有好多好多,涉及到范畴这些的,我就不过多了解了

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值