函数式编程最佳实践

这篇文章不会大讲什么是函数式编程,为何要使用函数式编程,而是开门见山,介绍函数式编程的最佳实践,但愿你们读完了这篇文章,会理解函数式编程的美妙而且爱上它。javascript

1. 纯函数

纯函数是这样一种函数:对于一样的输入,老是会产生一样的输出,没有反作用, 尽可能在你的代码中使用纯函数,这会使你的代码更加健壮,测试更加容易。java

接下来来一块儿看看各类反作用node

1.1 引用自由变量

看下面这段代码ajax

function foo(x) {
    y = x * 2;
  }
  var y;
  foo( 3 );
复制代码

分析上面的代码,调用函数改变了函数外的变量y, 产生了反作用,所以不是纯函数编程

当一个函数引用了函数外的变量,也就是自由变量,并非全部的自由变量引用都是糟糕的,可是咱们处理的时候必须很是当心api

咱们能够很是容易地将他变成纯函数数组

function foo(x) {
    return x*2;
}
foo( 3 );
复制代码

1.2 随机数

随机数也会产生反作用,对于一样的输入,结果是没法预测的bash

1.3 IO

最多见的反作用是输入输出,一个程序没有IO是彻底没有意义的,由于它的工做不能用任何方式观测到,举个最多见的例子app

var users = {};

function fetchUserData(userId) {
    ajax( `http://some.api/user/${userId}`, function onUserData(user){
        users[userId] = user;
    } );
}
复制代码

fetchUserData改变了users, 想变得更纯一点,咱们能够建立一个包裹函数safer_fetchUserData,将外部变量和不纯的函数都包裹起来函数式编程

function safer_fetchUserData(userId,users) {
    // 拷贝一份外部变量
    users = Object.assign( {}, users );

    fetchUserData( userId );

    return users;


    // ***********************

    // 原始的非纯函数:
    function fetchUserData(userId) {
        ajax(
            `http://some.api/user/${userId}`,
            function onUserData(user){
                users[userId] = user;
            }
        );
    }
}
复制代码

safer_fetchUserData更纯一点,可是依然不是纯函数,由于依赖ajax调用的结果,ajax的反作用是没法消除的 从上面也能够看出,反作用没法彻底消除,咱们只能尽量地写纯函数,将不纯的部分都收集在一块儿

2. 一元函数(单参数)

考虑下面这段代码

["1","2","3"].map( parseInt );
复制代码

相信不少人都会不假思索的回答[1,2,3], 可是真实的结果是[1, NaN, NaN], 认真思考一下array.map(fn)这个高阶函数的执行过程,在每一轮的迭代中,fn函数都会执行,执行的时候会传入三个参数, 分别是数组的这一轮的元素,索引,和数组自己,因此真实的执行状况是

parseInt("1", 0, ["1","2","3"])
parseInt("2", 1, ["1","2","3"])
parseInt("3", 2, ["1","2","3"])
复制代码

parseInt(x,radix)能够接受两个参数,由于第三个参数忽略,第一个参数x表明要转换的值, 第二个参数radix是转换进制,当参数 radix 的值为 0,或没有设置该参数时,parseInt() 会根据 string 来判断数字的基数。

当忽略参数 radix , JavaScript 默认数字的基数以下:

若是 string 以 "0x" 开头,parseInt() 会把 string 的其他部分解析为十六进制的整数。

若是 string 以 0 开头,那么 ECMAScript v3 容许 parseInt() 的一个实现把其后的字符解析为八进制或十六进制的数字。

若是 string 以 1 ~ 9 的数字开头,parseInt() 将把它解析为十进制的整数。 因此最后结果是[1, NaN, NaN]也就不足为奇了

那么如何解决这个问题,返回预期的结果, 估计不少人不假思索都会想出这种方式

["1","2","3"].map(v => parseInt(v));
复制代码

没错,的确能够获得正确的结果,简单粗暴,直接明了,可是这种方式是否是能够稍微封装一下,具备扩展性 在函数式编程中,能够用这个一元转换函数包裹目标函数,确保目标函数只会接受一个参数

function unary(fn) {
    return function onlyOneArg(arg){
        return fn(arg);
    };
}
["1","2","3"].map( unary(parseInt) ); // [1,2,3]
复制代码

3. 偏函数partial

固定一个函数的一个或者多个参数,返回一个新的函数,这个函数用于接受剩余的参数, 和map结合使用比较多

function partial(fn, ...presetArgs) {
        return function partiallyApplied(...laterArgs ) {
           return fn(...presetArgs, ...laterArgs )
       } 
    }
    // 应用1
    var getPerson = partial( ajax, "http://some.api/person" )
    var getOrder = partial( ajax, "http://some.api/order" )
    // version1
    var getCurrentUser = partial(ajax, "http://some.api/person", {user: "hello world"})
    // version 2
    var getCurrentUser = partial( getPerson, { user: CURRENT_USER_ID } );
    // 应用2
    function add(x, y){
        return x + y
    }
    [1,2,3,4,5].map( partial( add, 3 ) )
复制代码
function partialRight(fn, ...presetArgs) {
        return function partiallyApplied(...laterArgs) {
            return fn(...laterArgs, ...presetArgs)
        }   
    }
复制代码

4. 科里化 curry

柯里化是一种将使用多个参数的一个函数转换成一系列使用一个参数的函数的技术。

function add(a, b) {
        return a + b;
    }
    
    // 执行 add 函数,一次传入两个参数便可
    add(1, 2) // 3
    // 假设有一个 curry 函数能够作到柯里化
    var addCurry = curry(add);
    addCurry(1)(2) // 3
复制代码

下面是一个简单的实现

function sub_curry(fn) {
    var args = [].slice.call(arguments, 1);
    return function() {
        return fn.apply(this, args.concat([].slice.call(arguments)));
    };
}

function curry(fn, length) {
    length = length || fn.length;
    var slice = Array.prototype.slice;
    return function() {
        if (arguments.length < length) {
            var combined = [fn].concat(slice.call(arguments));
            return curry(sub_curry.apply(this, combined), length - arguments.length);
        } else {
            return fn.apply(this, arguments);
        }
    };
}



复制代码

5. 组合 compose

函数组合,将一个函数的输出当成另一个函数的输入,让数据流能够像水在水管中流动同样,为了组合,必须保证组合的函数参数只能有一个,并且必须有返回值
复制代码
// 执行顺序从右向左
    function compose(...fn) {
        return function composed(result){
            var list = [...fn]
            while(list.length > 0) {
                result = list.pop()(result)
            }
            return result
        }
    }
    // 管道函数, 从左向右移动
    function pipe(...fn) {
        return function piped(result) {
            var list = [...fn]
            while(list.length > 0) {
                result = list.shift()(result)
            }
            return result
        }
    }

复制代码

6. 递归(recursion)

// 判断一个数是否是素数
function isPrime(num,divisor = 2){
    if (num < 2 || (num > 2 && num % divisor == 0)) {
        return false;
    }
    if (divisor <= Math.sqrt( num )) {
        return isPrime( num, divisor + 1 );
    }
    return true;
}
// 计算二叉树的深度
function depth(node) {
    if(node) {
            let depthLeft = depth(node.left)
            let depthRight = depth(node.right)
            return 1 + Math.max(depthLeft, depthLeft)
    }
    return 0
}
复制代码
  • 递归太深,会存在内存溢出的问题,须要用尾调用来优化
// 解决栈溢出的问题,尾调用优化
    // 尾调用的概念很是简单,就是指某个函数的最后一步是调用另外一个函数。
    // 下面都不是
    // 状况一
    function f(x){
      let y = g(x);
      return y;
    }
    
    // 状况二
    function f(x){
      return g(x) + 1;
    }
    
    // 阶乘函数
    function factorial(n) {
        if( n === 1) {
            return 1
        }
        return n*factorial(n-1)
    }
复制代码

将阶乘函数改为尾调用, 确保最后一步只调用自身, 就是把全部用到的内部变量改写成函数的参数

function factorial(n, total) {
        if (n===1) {
          return total  
        }
        return factorial(n - 1, n*total)
    }
    // 可是这样会传两个参数,用两个函数改写一下
    function factorial(n) {
        return tailFactorial(n ,1)
    }
    function tailFactorial(n, total) {
        if (n===1) {
          return total  
        }
        return tailFactorial(n - 1, n*total)
    }
    
    // 继续改写, tailFactorial放在factorial内部
    function factorial(n) {
        function tailFactorial(n, total) {
            if (n===1) {
            return total  
            }
            return tailFactorial(n - 1, n*total)
        }
        return tailFactorial(n ,1)
    } 
    
    // 也可使用curry函数,将多参数的函数转换为单参数的形式
    function currying(fn, n) {
      return function (m) {
        return fn(m, n);
      };
    }
    function tailFactorial(n, total) {
        if (n===1) {
          return total  
        }
        return tailFactorial(n - 1, n*total)
    } 
    var factorial = currying(tailFactorial, 1)
    factorial(5)
复制代码
相关文章
相关标签/搜索