前端函数式编程的概念已经出现了蛮久了,我可能或多或少在项目中使用过函数式的方法写代码,可是我一直也没有仔细深刻的研究下什么是函数式编程,最近恰好有空,查了些资料,看了些书籍,把本身的心得总结下。html
要是想要弄明白函数式编程首先要明白什么是高阶函数。前端
高阶函数的定义是:git
函数能够做为参数被传递
函数能够做为返回值输出github
假如,咱们有这样一个数组:算法
const classA = [
{
name: '张三',
age: 17
},
{
name: '李四',
age: 15
},
{
name: '王五',
age: 16
},
]
复制代码
有一个需求,是找出班级中16岁年纪的学生,咱们使用低阶函数作筛选是这样的:编程
let student = [];
for (let i = 0; i < classA.length; i++) {
if (classA[i].age === 16) {
student.push(class[i])
}
}
复制代码
使用高阶函数是这样的:segmentfault
const student = classA.filter( v => v.age === 16 )
复制代码
那么使用这样的高阶函数有什么好处呢,有两点:数组
好比说,这样一个筛选学生的函数,能够拆成两部分:缓存
const isAge = v => v.age === 16;
const result = classA.filter(isAge);
复制代码
这样拆分后,逻辑就分为了两个部分,第一部分是判断年纪的函数,第二部分是筛选结果的函数。bash
若是,之后咱们的需求有了变化,不筛选学生年纪了,改为了筛选学生姓名,或者一些其它的东西,那么咱们只须要改动判断年纪的函数就好了,筛选结果的函数不变。
嗯,可能有人会说,这太简单了,那么,稍微来点难度的东西!
假如,咱们有这样一个数组:
const array = [['张三','26','1000'],['李四','25','3655'],['王五','30','8888']]
复制代码
咱们要把这个数组变成下面这种形式:
[
{
name: '张三',
age: '26',
price: '1000'
},
{
name: '李四',
age: '25',
price: '3655'
},
{
name: '王五',
age: '30',
price: '8888'
},
]
复制代码
使用高阶函数来作转换:
const result = array.reduce((value, item, index) => {
value[index] = {
name: item[0],
age: item[1],
price: item[2]
};
return value;
}, []);
复制代码
这里咱们使用了ES6的高阶函数reduce
,具体相关介绍能够去看凹凸实验室写的JavaScript中reduce()方法不彻底指南
ES6中自带的高阶函数,有filter
,map
,reduce
等等等等
ok,到了这里,已经对函数式编程有了些简单的概念了,我所理解的函数式编程是:
编写代码的时候,函数式编程更多的是从声明式的方法,而传统的编程更多的是命令式的方法。例如,上面的筛选学生年纪,传统的编程思想是,我建立了什么,我循环了什么,我判断了什么,得出了什么结果;函数式编程的思想是,我声明了一个筛选的函数,我声明了一个判断的函数,我把这两个函数结合起来,得出了一个结果。
当咱们玩了不少ES6自带的高阶函数后,就能够升级到本身写高阶函数的阶段了,好比说用函数式的方式写一个节流函数,
节流函数说白了,就是一个控制事件触发频率的函数,之前能够一秒内,无限次触发,如今限制成500毫秒触发一次
throttle(fn, wait=500) {
if (typeof fn != "function") {
// 必须传入函数
throw new TypeError("Expected a function")
}
// 定时器
let timer,
// 是不是第一次调用
firstTime = true;
// 这里不能用箭头函数,是为了绑定上下文
return function (...args) {
// 第一次
if (firstTime) {
firstTime = false;
fn.apply(this,args);
}
if (timer) {
return;
}else {
timer = setTimeout(() => {
clearTimeout(timer);
timer = null;
fn.apply(this, args);
},wait)
}
}
}
// 单独使用,限制快速连续不停的点击,按钮只会有规律的每500ms点击有效
button.addEventListener('click', throttle(() => {
console.log('hhh')
}))
复制代码
写好了这样一个高阶函数后,咱们就能够在各处调用了,好比:
// 有一个点击增长的功能,可是要求最少过了1秒才能增长一次,就能够
const add = x => x++;
throttle(add,1000);
// 又有了一个减小的功能,可是要求最少2秒减小一次
const cutDown = x => x--;
throttle(cutDown,2000);
复制代码
到这里已经明白了什么是高阶函数,可是还不够,还须要了解一些函数式编程的重要概念
在函数式编程的概念中,还有一个重要的概念是纯函数,那么什么是纯函数呢?
咱们用代码来解释什么是纯函数:
const z = 10;
add(x, y) {
return x + y;
}
复制代码
上面的add
函数就是一个纯函数,它读取x
和y
两个参数的值,返回它们的和,而且不会受到全局的z
变量的影响
把这个函数改一下
const z = 10;
add(x, y) {
return x + y + z;
}
复制代码
这个函数就变成了不纯的函数了,由于它返回的值会受到全局的z
的影响
换句话说,这个函数会被外部环境影响
so,咱们就得出了第一个判断是否纯函数的重要依据
一、纯函数不会受到外部环境的影响
再用splice
和slice
来解释一下:
var xs = [1,2,3,4,5];
// 纯的
xs.slice(0,3);
//=> [1,2,3]
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]
xs.splice(0,3);
//=> []
复制代码
slice
收到一样的参数,每次返回相同的值,因此是纯函数
splice
收到一样的参数,每次返回不一样的值,因此不是纯函数
so,咱们就得出了第二个判断是否纯函数的重要依据
二、纯函数相同的输入,永远会获得相同的输出
来个总结,纯函数是:
'纯函数是这样一种函数,即相同的输入,永远会获得相同的输出,并且没有任何可观察的反作用'
复制代码
那么什么是反作用
,在纯函数里有这样一个定义:
一切函数自己计算结果以外发生的事情都叫作反作用
像是上面的例子,函数返回的结果受到外部z
变量影响,那么这个函数是有反作用的,反之,函数影响了外部环境,也是有反作用的。
到了这里,终于弄明白了什么是纯函数,它有如下的优势
使用纯函数可以极大的下降编程的复杂度,可是不合理的使用,为了抽象而去抽象,反而会使代码变得很是难以理解。
柯里化的概念很简单:只传递给函数一部分参数来调用它,让它返回一个函数去处理剩下的参数。
const add = x => y => x + y;
add(1)(2);
// => 3
复制代码
上面的例子,就是一个很典型的柯里化函数,在咱们第一次调用的时候,接收了第一次传入的参数(用闭包记住),返回了一个新的函数;在第二次调用的时候,接收第二次传入的参数,而且和第一次传入的函数相加,返回它们的和。
这个例子说明了柯里化的一个特征,或者说是一个基础,即柯里化函数有延迟求值的特殊性,而这种特殊性又须要用到一些手段来实现。
运用上面的思想编写一个的柯里化函数
// 建立柯里化函数,保存了第一次传入的参数和函数,返回值是一个函数而且接收第二次传入参数,同时调用传入的函数进行计算
currying (fn, ...args1) {
return (...args2) => {
return fn(...args1, ...args2)
}
}
// 定义一个通常函数
const add = (x, y) => x + y;
// 使用
const increment = currying(add, 1);
console.log(increment(2));
const addTen = currying(add, 10);
console.log(addTen(2));
// => 3
// => 12
复制代码
这个列子还有点小问题,即返回的值没有自动柯里化,能够改造下:
currying(fn, ...args1) {
// '判断传入的参数是否知足传入函数须要的参数,好比说add函数须要两个参数相加,那么判断是否传入了两个参数,知足调用传入函数计算结果'
if (args1.length >= fn.length) {
console.log(args1, '--1--');
return fn(...args1);
}
// '不知足返回一个新的函数,继续调用柯里化函数,传入保存的第一次传入的函数,传入保存的第一次传入的参数,传入第二次传入的参数,继续上面的判断逻辑,返回计算结果'
return (...args2) => {
console.log(args2, '--2--');
return currying(fn, ...args1, ...args2);
};
},
// 定义一个通常函数
const add = (x, y) => x + y;
// 使用
const increment = currying(add, 1);
console.log(increment(2));
const addTen = currying(add, 10);
console.log(addTen(2));
// => [2] --2--
// => [1,2] --1--
// => 3
// => [2] --2--
// => [10,2] --1--
// => 12
复制代码
函数在js中是一等公民,它和其它对象,或者其它数据没有什么区别,能够存在数组,存在对象,赋值给变量,看成参数传来传去,因此函数也有下标属性,用上面的例子证实一下
const add = (x, y) => x + y;
console.log(add.length)
// => 2
复制代码
在ES6中,...
是扩展运算符,他的使用是这样的
// 放在函数做为单独参数,会把一个数组变成参数序列,好比上面例子中的数组[1,2]变成了参数x=1,y=2
fn(...args1)
// 放在函数中做为第二个参数,会把传入的值变成一个数组,若是传入的是一个数组那么仍是数组,传入一个对象,会变成一个数组对象
function currying(fn,...x) {
console.log(x)
}
currying(0,1)
// => [1]
// 放在回调函数中做为第二个和第三个参数
// 第一次调用会返回一个函数,会在闭包里存贮值,第二次调用会把闭包里的值和第二次参数里的值合并成数组
return currying(fn, ...args1, ...args2);
// => [1,2]
// 可是单独在函数中这么使用会报错
function currying(fn,...x,...y) {
console.log(x)
}
currying(0,1,2)
复制代码
理解了这些,上面的例子就很好懂了。
柯里化函数比较重要的思想是:
屡次判断传入的参数是否知足计算需求,知足,返回计算结果,若是不知足,继续返回一个新的柯里化函数
上面的柯里化函数还能够继续优化,好比说,this绑定啊,特殊的变量占位符啊,等等,这样的工做,一些库,好比说ramda
已经实现,能够去看它的源代码里面是怎样实现的,重点仍是要明白柯里化函数是怎么一回事。
首先,先写一个简单的组合函数:
const compose = (f, g) => x => f(g(x));
复制代码
这个组合函数接收两个函数看成参数,而后返回一个新的函数,x是两个函数之间都要使用的值,好比说:
// 咱们要实现一个给字符串所有变成大写,而后加上个感叹号的功能,只须要定义两个函数,而后组合一下
const toUpperCase = x => x.toUpperCase();
const exclaim = x => `${x}!`;
const shout = compose(exclaim, toUpperCase);
shout('hello world')
// => HELLO WORLD!
复制代码
注意:组合函数里面,g
函数比f
函数先执行,因此在组合里面,是从右往左执行的,也就是说,要把先执行的函数放在组合函数的右边
这个组合函数仍是有点问题,它只能接收2个参数,咱们来稍微改造下,让它变得强大点:
const compose = (...fns) => (...args) => fns.reduceRight((res, fn) => [fn.call(null, ...res)], args)[0];
// 使用,实现一个功能,字符串变成大写,加上个感叹号,还要截取一部分,再在前面加上注释
const toUpperCase = x => x.toUpperCase();
const exclaim = x => `${x}!`;
const head = x => `slice is: ${x}`;
const reverse = x => x.slice(0, 7);
const shout = compose(exclaim, toUpperCase, head, reverse)
shout('my name is maya')
// => SLICE IS: MY NAME!
复制代码
组合的原理其实就是数学中的结合律:
(a + b) + c = a + (b + c)
复制代码
so,在组合中你能够这样
// 第一种
const one = compose(exclaim, toUpperCase)
const shout = compose(one, head, reverse)
shout('my name is maya')
// => SLICE IS: MY NAME!
// 第二种
const two = compose(toUpperCase, head)
const shout = compose(exclaim, two, reverse)
shout('my name is maya')
// => SLICE IS: MY NAME!
// 第三种
const three = compose(head, reverse)
const shout = compose(exclaim, toUpperCase, three)
shout('my name is maya')
// => SLICE IS: MY NAME!
...
复制代码
so,到了这里,我对组合的理解是:
组合是什么,组合就是运用了数学里的结合律,像是搭积木同样,把不一样的函数联系起来,让数据在里面流动
在各类库里面都有组合的函数,lodash
,underscore
,ramda
等等,好比在underscore
里面,组合是这样的:
// Returns a function that is the composition of a list of functions, each
// consuming the return value of the function that follows.
_.compose = function() {
var args = arguments;
var start = args.length - 1;
return function() {
var i = start;
var result = args[start].apply(this, arguments);
while (i--) result = args[i].call(this, result);
return result;
};
};
复制代码
嗯,到了这里,已经初步了解了函数式编程的概念了,那么咱们怎么使用函数式编程的方式写代码呢,举个例子:
// 伪代码,思路
// 好比说,咱们请求后台拿到了一个数据,而后咱们须要筛选几回这个数据, 取出里面的一部分,而且排序
// 数据
const res = {
status: 200,
data: [
{
id: xxx,
name: xxx,
time: xxx,
content: xxx,
created: xxx
},
...
]
}
// 封装的请求函数
const http = xxx;
// '传统写法是这样的'
http.post
.then(res => 拿到数据)
.then(res => 作出筛选)
.then(res => 作出筛选)
.then(res => 取出一部分)
.then(res => 排序)
// '函数式编程是这样的'
// 声明一个筛选函数
const a = curry()
// 声明一个取出函数
const b = curry()
// 声明一个排序函数
const c = curry()
// 组合起来
const shout = compose(c, b, a)
// 使用
shout(http.post)
复制代码
我以为,想要在项目里面正式使用函数式编程有这样几个步骤:
ramda
的库来编写代码ramda
的过程当中,能够尝试研究它的源代码固然了,这个只是我本身的理解,我在实际项目中也没有彻底的使用函数式编程开发,个人开发原则是:
不要为了函数式而选择函数式编程。若是函数式编程可以帮助你,可以提高项目的效率,质量,可使用;若是不能,那么不用;若是对函数式编程还不太熟,好比我这样的,偶尔使用
函数式编程是在范畴论的基础上发展而来的,而关于函数式编程和范畴论的关系,阮一峰大佬给出了一个很好的说明,在这里复制粘贴下他的文章
本质上,函数式编程只是范畴论的运算方法,跟数理逻辑、微积分、行列式是同一类东西,都是数学方法,只是碰巧它能用来写程序
因此,你明白了吗,为何函数式编程要求函数必须是纯的,不能有反作用?由于它是一种数学运算,原始目的就是求值,不作其余事情,不然就没法知足函数运算法则了。
本人水平有限,有错漏之处,望大佬们多多指出,同时轻喷!!!
利用函数式编程封装节流和防抖函数
学会JavaScript函数式编程(第1部分)
Mostly adequate guide to FP
合理的使用纯函数式编程
函数式编程入门教程
大佬,JavaScript 柯里化,了解一下?