内容简介:前端函数式编程的概念已经出现了蛮久了,我可能或多或少在项目中使用过函数式的方法写代码,但是我一直也没有仔细深入的研究下什么是函数式编程,最近刚好有空,查了些资料,看了些书籍,把自己的心得总结下。要是想要弄明白函数式编程首先要明白什么是高阶函数。高阶函数的定义是:
前端函数式编程的概念已经出现了蛮久了,我可能或多或少在项目中使用过函数式的方法写代码,但是我一直也没有仔细深入的研究下什么是函数式编程,最近刚好有空,查了些资料,看了些书籍,把自己的心得总结下。
高阶函数的定义
要是想要弄明白函数式编程首先要明白什么是高阶函数。
高阶函数的定义是:
函数可以作为参数被传递
函数可以作为返回值输出
使用ES6自带的高阶函数来编写代码
假如,我们有这样一个数组:
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]) } } 复制代码
使用高阶函数是这样的:
const student = classA.filter( v => v.age === 16 ) 复制代码
那么使用这样的高阶函数有什么好处呢,有两点:
- 第一,同等复杂度的代码,高阶函数能让实现更加简单
- 第二,高阶函数能够非常方便的拆分逻辑
比如说,这样一个筛选学生的函数,可以拆成两部分:
const isAge = v => v.age === 16; const result = classA.filter(isAge); 复制代码
这样拆分后,逻辑就分为了两个部分,第一部分是判断年纪的函数,第二部分是筛选结果的函数。
如果,以后我们的需求有了变化,不筛选学生年纪了,改成了筛选学生姓名,或者一些其它的东西,那么我们只需要改动判断年纪的函数就行了,筛选结果的函数不变。
嗯,可能有人会说,这太简单了,那么,稍微来点难度的东西!
假如,我们有这样一个数组:
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,我们就得出了第一个判断是否纯函数的重要依据
1、纯函数不会受到外部环境的影响
再用 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,我们就得出了第二个判断是否纯函数的重要依据
2、纯函数相同的输入,永远会得到相同的输出
来个总结,纯函数是:
'纯函数是这样一种函数,即相同的输入,永远会得到相同的输出,而且没有任何可观察的副作用' 复制代码
那么什么是 副作用
,在纯函数里有这样一个定义:
一切函数本身计算结果之外发生的事情都叫做副作用
像是上面的例子,函数返回的结果受到外部 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 WOELD! 复制代码
注意:组合函数里面, 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
当然了,这个只是我自己的理解,我在实际项目中也没有完全的使用函数式编程开发,我的开发原则是:
不要为了函数式而选择函数式编程。如果函数式编程能够帮助你,能够提升项目的效率,质量,可以使用;如果不能,那么不用;如果对函数式编程还不太熟,比如我这样的,偶尔使用
扩展
函数式编程是在范畴论的基础上发展而来的,而关于函数式编程和范畴论的关系,阮一峰大佬给出了一个很好的说明,在这里复制粘贴下他的文章
本质上,函数式编程只是范畴论的运算方法,跟数理逻辑、微积分、行列式是同一类东西,都是数学方法,只是碰巧它能用来写程序
所以,你明白了吗,为什么函数式编程要求函数必须是纯的,不能有副作用?因为它是一种数学运算,原始目的就是求值,不做其他事情,否则就无法满足函数运算法则了。
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
HotSpot实战
陈涛 / 人民邮电出版社 / 2014-3 / 69
《HotSpot实战》深入浅出地讲解了HotSpot虚拟机的工作原理,将隐藏在它内部的本质内容逐一呈现在读者面前,包括OpenJDK与HotSpot项目、编译和调试HotSpot的方法、HotSpot内核结构、Launcher、OOP-Klass对象表示系统、链接、运行时数据区、方法区、常量池和常量池Cache、Perf Data、Crash分析方法、转储分析方法、垃圾收集器的设计演进、CMS和G......一起来看看 《HotSpot实战》 这本书的介绍吧!