函数式编程

概念:函数式编程是一种将电脑运算视为函数运算的编程范式(程序设计)。

  1. // 伪代码
  2. // 函数式编程
  3. function fb (n) {
  4. if ( n <= 1 ) {return 1};
  5. return fb(n - 1) + fb(n - 2);
  6. }
  7. // 指令式
  8. function fb (n) {
  9. let arr = [1,1]
  10. for(let i=2;i<n;i++){
  11. arr[i] = arr[i-1] + arr[i-2]
  12. }
  13. return arr[n-1]
  14. }

要求:在函数式编程中,函数是一等公民。所谓一等公民,其实就是普通公民,其他数据类型一样,没有什么特殊的,我们可以像对待任何其他数据类型一样对待函数。

  1. function delay() {
  2. console.log('5000ms 后执行 delay 方法!')
  3. }
  4. setTimeout(() => {
  5. delay()
  6. }, 5000)
  7. setTimeout(delay, 5000)

思想:强调程序执行的结果而非执行的过程
我们在使用函数时,只需要关心该函数实现了什么功能,需要什么参数,而不需要过多的关心函数的内部实现,就能够顺利的将函数运用起来。我们使用的各种工具都是同样的思想,只需要知道函数的功能和参数即可。

纯函数

定义:相同的输入总会得到相同的输出,并且不会产生副作用的函数。
可靠性:
因为没有副作用,所以可靠。使用纯函数不必考虑任何的数据变化。

  1. let source = [1, 2, 3, 4, 5];
  2. source.slice(1, 3); // 纯函数 返回[2, 3] source不变
  3. source.splice(1, 3); // 不纯的 返回[2, 3, 4] source被改变
  4. source.pop(); // 不纯的
  5. source.push(6); // 不纯的
  6. source.shift(); // 不纯的
  7. source.unshift(1); // 不纯的
  8. source.reverse(); // 不纯的
  9. source = [1, 2, 3, 4, 5];
  10. function getLast(arr) {
  11. return arr[arr.length-1];
  12. }
  13. function getLast_(arr) {
  14. return arr.pop();
  15. }
  16. console.log(getLast(source),source);
  17. console.log(getLast_(source),source);

可移植性:
我们的utils文件里的方法绝大多数都是纯函数,可以一次封装多次使用。例如getParams(url, param)输出结果只和参数有关且无副作用。
可缓存性:
因为相同的输入总能得到相同的输出,因此,如果函数内部计算非常复杂,当我们发现输入与上一次相同时,可以直接返回结果而不用经过内部的计算。这是一种性能优化的策略。

  1. // 传入日期,获取当天的数据
  2. function process(date) {
  3. var result = '';
  4. // 假设这中间经历了复杂的处理过程
  5. return result;
  6. }
  7. function withProcess(base) {
  8. var cache = {}
  9. return function() {
  10. var date = arguments[0];
  11. if (cache[date]) {
  12. return cache[date];
  13. }
  14. cache[date] = base.apply(base, arguments);
  15. return cache[date]
  16. }
  17. }
  18. var _process = withProcess(process);
  19. // 经过上面一句代码处理之后,我们就可以使用_process来获取我们想要的数据,
  20. // 如果数据存在,会返回缓存中的数据,
  21. // 如果不存在,则会调用process方法重新获取。
  22. _process('2017-06-03');
  23. _process('2017-06-04');
  24. _process('2017-06-05');

高阶函数

在数学和计算机科学中,高阶函数是至少满足下列一个条件的函数:

  • 接受一个或多个函数作为输入
  • 输出一个函数

定义:以函数作为参数或者返回值的函数

函数作为参数
数组map:

  1. Array.prototype.myMap = function(fn,context){
  2. let tem = []
  3. if(typeof fn=='function'){
  4. let k = 0
  5. const len = this.length
  6. while(k<length){
  7. tem.push(fn.call(context,this[k],k))
  8. k++
  9. }
  10. return temp
  11. }
  12. }

由此可见,map方法中的循环过程是公共逻辑,而具体对每一项做什么则由fn参数传入,来让使用者自定义。我们可将传入的函数成为基础函数,而map方法就可以称为高阶函数。类似:数组filter、promise里的回调

函数作为返回值

  1. // 根据参数type返回不同的类型判断函数
  2. function isType(type){
  3. return function (obj){
  4. return Object.prototype.toString.call( obj ) === '[object '+type+']';
  5. }
  6. };

h5组件库中的createNamespace

混合应用

  1. // 单例模式
  2. var getInstance = function(fn) {
  3. var result;
  4. return function(){
  5. return result || (result = fn.call(this,arguments));
  6. }
  7. };
  8. //防抖
  9. function debounce(fn,delay=1000){
  10. let timeout = null
  11. return function(){
  12. clearTimeout(timeout)
  13. timeout = setTimeout(()=>{
  14. fn.apply(this,arguments)
  15. },delay)
  16. }
  17. }
  18. // 节流
  19. function throttle(fn,delay=1000){
  20. let run = true
  21. return function(){
  22. if (!canRun) return;
  23. canRun = false;
  24. setTimeout(()=>{
  25. fn.apply(this,arguments)
  26. canRun = true
  27. },delay)
  28. }
  29. }

这里高阶函数的优势:1、封装公共逻辑;2、动态定义;3、在需要执行时调用
多熟悉一些成熟的应用场景并尝试在代码中使用,以便写出结构更合理、逻辑更清晰的代码。

函数柯里化

柯里化是高阶函数的一种用法。
定义:是把接受多个参数的函数变换成接受一个单一参数的函数,并且返回接受余下的参数的新函数的技术。 (一个函数(比如叫curry)接受一个函数A为参数,返回一个新函数curA,A(m,n) = curA(m)(n))
常见的一道题:

  1. function add(a, b, c) {
  2. return a+b+c
  3. }
  4. let _add = curry(add);
  5. _add(a)(b,c)
  6. _add(a)(b)(c)

函数add被curry后得到柯里化函数_add,_add能够处理add的所有剩余参数。因此柯里化也被称为部分求值。
简版实现:

  1. // argsNow用于收集参数
  2. // lenNow用于标记还剩几个参数没传进来,全部传进来了就可以执行了
  3. function curry(fun,args=[],len=fun.length){
  4. // 第一次创建_add函数的时候,没传args和len,也就是初始化函数时,参数数组为空
  5. let argsNow = args
  6. let lenNow = len
  7. function changeFun(){
  8. // 把参数收集起来
  9. let rArgs = [...arguments]
  10. argsNow = [...argsNow,...arguments]
  11. // 如果没收集完就继续收集
  12. if(rArgs.length<len){
  13. lenNow -= rArgs.length
  14. return curry(fun,argsNow,lenNow)
  15. }
  16. // 收集完了就执行
  17. return fun(...argsNow)
  18. }
  19. return changeFun
  20. }

花里胡哨!最后执行结果不还是执行了原函数?!柯里化就这?简单的问题反而给搞复杂了。

  1. // 前面高阶函数的一个案例
  2. function isType(type,obj){
  3. return Object.prototype.toString.call( obj ) === '[object '+type+']';
  4. };
  5. // 请求
  6. function ajax(method, url, data) {
  7. return fetch(url, {
  8. method: method, // or 'PUT'
  9. body: data
  10. })
  11. }

通过柯里化我们可以得到:

  1. let _isType = curry(isType)
  2. let _ajax = curry(ajax)
  3. let isString = _isType('string')
  4. let isObject = _isType('object')
  5. let getAjax = _ajax('get')
  6. let postAjax = _ajax('post')

**

再看一个更具高阶函数思维的例子:
开发中,常用的数组map通常只有几类,所以我们可以做出如下封装。

  1. function arrMap(func, array) {
  2. return array.map(func);
  3. }
  4. let _arrMap = createCurry(arrMap);
  5. let percenMap = _getNewArray(function(item) {
  6. return item * 100 + '%'
  7. })
  8. let dubbleMap = _getNewArray(function(item) {
  9. return item * 2
  10. })
  11. percenMap([0.01, 1]);
  12. dubbleMap([10,20])

经过这个过程我们发现,柯里化能够应对更加复杂的逻辑封装。当情况变得多变,柯里化依然能够应付自如。
而且柯里化后的函数也明显的更加自由。
**
前面的高阶函数大家可以在实践中多尝试使用,但是柯里化不建议为了使用而使用。因为在js中,柯里化的实现是以性能为代价的,只有当情况变得复杂时,才是柯里化的主场。
尽管我们的例子都太简单了,简单到对他们进行柯里化显得多余,但是我还是希望能在这些简单的案例中学到柯里化的思维。在未来实践中,如果发现用普通的思维封装一些逻辑慢慢变得困难,最起码可以想到柯里化这样一个方案。
包括柯里化和前面所有的函数式编程的内容,当中没有什么新奇的东西或者要记的概念,更多的是一些编程思想上的东西,希望能对大家实际开发当中产生一点点的影响。