JavaScript中的函数式编程

常见的编程范式有:面向对象编程(Java、JavaScript)、面向过程编程(C)、声明式编程(SQL)、命令式编程、函数式编程等。

1.函数式编程与命令式编程的区别?

(1)命令式编程的主要思想是关注计算机的执行步骤,即一步一步告诉计算机先做什么再做什么。
(2)函数式编程的主要思想是告诉计算机应该做什么,但不指定具体要怎么做。

举个例子:
    **将数组中的每一项乘以2,得到一个新数组**
    //命令式
    let newArr = [];
    for(let i=0;i<arr.length;i++){
      newArr.push(arr[i]*2);
    }
    //函数式
    let newArr = arr.map(function(item,index){
      return item*2;
    })
复制代码

2.函数式编程的特点?

(1)函数是第一等公民。

函数与其他数据类型一样,可以赋值给其他变了,也可以作为函数传递给其他函数,还可以作为别的函数的返回值。

这里的概念与js中高阶函数的概念非常相似。高阶函数是指至少满足下列条件之一的函数 函数可以作为参数被传递;函数可以作为返回值输出。

举个例子
      **节流函数**
      const throttle = (func, wait = 50) => {
      let lastTime = 0
      return function (...args) {
        let now = +new Date()
        // 将当前时间和上一次执行函数时间对比
        // 如果差值大于设置的等待时间就执行函数
        if (now - lastTime > wait) {
          lastTime = now
          func.apply(this, args)
        }
      }
    }
    // 使用节流
    window.onresize = throttle(function () {
      console.log('throttle')
    }, 600)
复制代码

(2)函数是纯函数

纯函数的定义:一个函数如果输入参数确定,输出结果是唯一确定的,那么它就是纯函数。

纯函数的特点:无状态,无副作用,无关时序,幂等(无论调用多少次,结果相同)即仅取决于提供的输入,而不依赖于任何在函数求值或调用间隔时可能变化的隐藏状态和外部状态。不会造成超出作用域的变化,例如修改全局变量或引用传递的参数。

举个例子
       let arr = [1, 2, 3];

        arr.slice(0, 3); //是纯函数

        arr.splice(0, 3); //不是纯函数,对外有影响

        function add(x, y) { // 是纯函数
          return x + y // 无状态,无副作用,无关时序,幂等
        } // 输入参数确定,输出结果是唯一确定

        let count = 0; //不是纯函数
        function addCount() { //输出不确定
          count++ // 有副作用
        }

        function random(min, max) { // 不是纯函数
          return Math.floor(Math.radom() * (max - min)) + min // 输出不确定
        } // 但注意它没有副作用
        
        function setColor(el, color) { //不是纯函数
          el.style.color = color; //直接操作了DOM,对外有副作用
        }
复制代码

在阮一峰的博文中提到了函数式编程的5个特点:函数是第一等公民;只用表达式,不用语句;没有副作用;不修改状态;引用透明。第二点后来也提到在实际开发过程中,不考虑系统的读写,不用语句是不可能的,只能尽量减少。而3,4,5点恰好就是纯函数的特点。
(参考链接)

3.函数式编程的应用

    **计算(a+b-c)*d的值**   
    // 一般写法
    let result = (a+b-c)*d;
    //函数式写法
    function sum(a,b){
      return a+b;
    }
    function minus(a,b){
      return a-b;
    }
    function multify(a,b){
      return a*b;
    }
    let result = multify(minus(sum(a,b),c),d);
复制代码

这种写法虽然拆分了函数有一定的可复用性,但既不直观也不优雅。这里就有了函数式编程的两个基本运算:合成compose和柯里化currying。

    //函数式写法
    function sum(a,b){
      return a+b;
    }
    function minus(a,b){
      return a-b;
    }
    function multify(a,b){
      return a*b;
    }
    function compose(x,y,z){
      return function(a,b,c,d){
        return x(y(z(a,b),c),d)
      }
    }
    let calculate = compose(multify,minus,sum);
    let result = calculate(a,b,c,d);
复制代码

这样的compose函数不具有太强的可复用性,只能接受三个函数做参数。

    function compose() {
        let args = arguments;
        return function () {
            let params = arguments;
            // 传递进来的函数自后向前调用
            let start = args.length - 1;
            let result;
            while (start >= 0) {
                // 获取每个函数接收的参数的数量
                let paramNum = args[start].length;
                // 当result不为undefined时,result会被作为参数传递给下一个函数,则截取的参数数量-1
                paramNum = typeof result ==="undefined"?paramNum:paramNum-1;
                //截取当前函数接收的参数
                let paramForFn = [].splice.call(params, 0, paramNum);
                if (typeof result === "undefined") {
                    // 第一个函数调用时不接收其他函数的返回值做参数
                    result = args[start].apply(this, paramForFn);
                } else {
                    // 除第一个外,需接收其他函数的返回值做参数
                    result = args[start].apply(this, [].concat(result, paramForFn));
                }
                start--;
            }
            return result;
        }
    }
复制代码

试用一下

    let calculate = compose(multify,sum,sum,minus);
    calculate(2,1,2,5,2);//16
    function addHello(str){
        return 'hello '+str;
    }
    function toUpperCase(str) {
        return str.toUpperCase();
    }
    function reverse(str){
        return str.split('').reverse().join('');
    }
    var composeFn=compose(reverse,toUpperCase,addHello);
    composeFn("abc");//"CBA OLLEH"
复制代码

现在的compose函数可以接收任意数量的参数,可以任意组合纯函数,但是语义不够明确,现在的代码写起来是compose(multify,sum,sum,minus)(2,1,2,5,2)
函数式编程希望能够用一种更直观的形式调用,类似于minus(2,1).sum(2).sum(5).multify(2),这里就有了函子的概念。(参考链接)

如果想实现上述的结果,则每次函数调用后都应该将结果记录下来,并且返回的对象可以继续调用其他函数。下面这个函数有一个属性result用来记录上一次函数的执行结果,并返回一个新的Functor实例,每个实例都有一个map方法,每个map方法可以接收函数作为参数并再次返回一个Functor实例。

   function Functor(result) {
   // 利用result记录函数执行后的结果,以便传给下一个函数调用
       this.result = result;
   }
   Functor.prototype.map = function () {
   // 第一个参数为函数
   let fn = arguments[0];
   // 获取穿给函数的其他参数
   let param = [].slice.call(arguments, 1);
   // 第一次调用时没有result可以传给本次函数
   param = this.result ? [].concat(this.result, param) : param;
   // 将本次函数apply后的结果再传给Functor,则result即为本次函数的执行结果
   return new Functor(fn.apply(this, param));
   }
   // 调用
   new Functor().map(sum,1,2).map(minus,1).map(multify,4);
复制代码

到这里基本就完成了一个简单的函数式编程。但这个函数里,仍有很多可以优化的地方,像是需要new关键字。并不是严格意义上的链式调用,仍然需要调用map等。可以参考以下来实现一个真正意义上的链式调用。(参考链接)

但是通过这些编程体验我们不难发现,函数式编程采用声明式的风格,易于推理,提高代码的可读性。将函数视为积木,通过高阶函数来提高代码的模块化和可重用性。

举个例子

这里需要用到柯里化的概念:柯里化(Currying)是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数且返回结果的新函数的技术。

有一个用户数组arr,其存储的数据如下:

       const arr = [
            {
                id: 1,
                sex: "girl",
                age: 25,
                name: "aa",
                province: "bj"
            },
            {
                id: 2,
                sex: "boy",
                age: 29,
                name: "bb",
                province: "sh"
            },
            {
                id: 3,
                sex: "girl",
                age: 19,
                name: "cc",
                province: "gz"
            },
            {
                id: 4,
                sex: "girl",
                age: 14,
                name: "dd",
                province: "bj"
            },
            {
                id: 5,
                sex: "boy",
                age: 35,
                name: "ee",
                province: "bj"
            }
        ]
        //需求:编写一个过滤用户信息的函数,统计一下18岁以上的男性有多少人
        //命令式
        let result = [];
        for (let i = 0; i < arr.length; i++) {
            if (arr[i].age > 18 && arr[i].sex === "boy") {
                result.push(arr[i]);
            }
        }
        //函数式
        function filtAge(key, val, item) {
            return item[key] > value;
        }
        function filtSex(key, val, item) {
            return item[key] == val;
        }
         //通用柯里化函数
        var curry = function (fn, args) {
            //将数组的slice方法保存下来方便其他类数组对象调用
            var slice = Array.prototype.slice;
            //fn函数接收的参数个数
            var len = fn.length;
            //将fn以外的其他参数保存起来
            var _args = args ? slice.call(arguments, 1) : [];
            return function () {
                //将以前存储的参数与新传递的参数结合起来
                var argsNew = _args.concat(slice.call(arguments));
                //参数数量大于fn需要的参数数量时执行函数,否则继续存储参数
                if (argsNew.length >= len) {
                    return fn.apply(this, argsNew);
                } else {
                    return curry.call(this, fn, ...argsNew);
                }
            }
        }
        let filtAge18 = curry(filtAge, "age", 18);
        let filtSexBoy = curry(filtSex, "sex", "boy");
        let result = arr.filter(filtAge18).filter(filtSexBoy);

        //需求:编写一个过滤用户信息的函数,统计一下20岁以上的女性有多少人
        //命令式
        let result = [];
        for (let i = 0; i < arr.length; i++) {
            if (arr[i].age > 20 && arr[i].sex === "girl") {
                result.push(arr[i]);
            }
        }
        //函数式
        let filtAge20 = curry(filtAge, "age", 20);
        let filtSexGirl = curry(filtSex, "sex", "girl");
        let result = arr.filter(filtAge20).filter(filtSexGirl);
复制代码

从实际代码出发的话,函数式编程很多时候都不是最优解,js编写肯定要涉及到ajax,node常常会涉及到读写文件,这些场景都不能编写一个纯函数。到底是选择面向对象编程、函数式编程或者命令式编程还是应给结合实际的业务场景。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享