函数式编程
本文来自我的github转载时请注明出处0.前言本文并不是完全按照严格意义的函数式编程来讲,主要是抽取一些思想和js结合,以达到写出更有水平的代码的目的。1.概念1.1介绍在计算机科学里,函数式编程是一种编程范式,它将计算描述为表达式求值并避免了状态和数据改变。函数式编程(FP)思想像数学计算,比如数学中有一个函数为f(x)=x+1,那么f(0)=1,输出结果只...
本文来自我的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语句。
- 用===,避免==的自动类型转换
….
还有好多好多,涉及到范畴这些的,我就不过多了解了
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)