函数——高阶函数(函数式编程&函数柯里化&compose函数)

  一、概念

    了解高阶函数前我们先要了解什么是函数式编程,什么是一等函数。

    函数式编程:函数式编程是一种编程方式,支持函数作为第一类对象,是一种强调以函数使用为主的软件开发风格。函数式编程的主要目的是使用函数来抽象作用在数据之上的控制流和操作,从而在系统中消除副作用并减少对状态的改变。它属于“结构化编程”的一种主要思想是把运算过程尽量写成一系列嵌套的函数调用。JavaScript,Scala等是实现函数式编程的一些语言。

    一等函数:也可以理解成函数是“第一等公民”。所谓的“第一等公民”指的是函数与其它的数据类型一样,可以赋值给其它变量也可以作为参数进行传递或者是作为函数的返回值。在js中,函数是一种特殊类型的对象(Functioin对象)。js将函数视为一等公民,因为函数与其它数据类型一样,对于可以使用其它数据类型执行的操作,函数都是可以执行的,所以js中的函数可以被称之为一等函数。

    <script>
        //一等函数
            //把函数赋值给变量
            var fn = function () { };
            //函数作为参数
            setInterval(() => {
                console.log('hello world!');
            }, 1000);
            //函数作为返回值
            function Test() {
                return function () {
                 console.log('hello world');
             }
            }
            var res = Test();
            console.log(res);  //ƒ (){ console.log('hello world') }
    </script>

    高阶函数:高阶函数的英文是"Higher-order-function"。指的是操作其它函数的函数,一般来说,有两种情况:函数作为参数被传递;函数可以作为返回值输出。js的函数

  二、高阶函数的例子

  函数作为参数用于回调函数。回调函数是一个函数作为参数传递给另一个主函数里面(otherFunction),当那一个主函数执行完后,再执行传入的作为参数的函数。被作为参数传递到主函数的那个函数就叫做回调函数。

    <script>
        function title(value) {//回调函数
            alert(value);
        }
        function main(title, value) { //主函数,title当作参数,value这个值正是title()函数需要的
            alert('我是主函数');
            title(value); //  这行的title()是回调函数  
        }
        main(title, '我是回调函数') //调用的是main函数,先执行main()这个主函数,title()被main()在函数体中执行一次,更能体现title()是回调函数
    </script>

  函数作为参数使用,用于数组Array.prototype.map, Array.prototype.reduce, Array.prototype.filter,Array.prototype.sort。下面分别来介绍。

   Array.prototype.map

    语法:var new_array=arr.map(callback [,thisArg])

    定义:map方法返回一个新数组,数组中的元素为原始数组元素调用函数处理的后值。  

    参数:callback生成新数组元素的函数,具有三个参数(currentValue:当前元素必需,index:当前元素的索引可选,array:map方法调用的数组,可选)

       thisArg:执行callback函数时值被用作this,可选。

    返回值:一个新的数组,对原数组没有影响。

    <script>
        function fn(arr) {
            return arr.map(function (item) {
                return item * item;
            })
        }
        var arr = [1, 2, 3, 4];
        console.log(fn(arr));  //(4) [1, 4, 9, 16]

        const birthYear = [1994, 1998, 2003, 1987];
        const ages = birthYear.map(year => 2019 - year);
        console.log(ages); //(4) [25, 21, 16, 32]
    </script>

  Array.prototype.reduce

    语法:var new_array=arr.reduce(callback [,initialValue])

    定义:reduce方法接收一个函数作为累加器,对数组的每个成员执行回调函数。

    参数:callback执行数组中的每个值(如果没有提供initialValue则第一个值除外),有四个参数(accumulator累计器累计回调的返回值必需,currentValue:数组中正在处理的数组必需,index:索引可选,array:数组可选)

       initialValue: 作为第一次调用callback函数时的第一个参数的值,如果没有提供初始值,那就使用数组中的第一个元素,在没有初始值的空数组上调用reduce方法报错。

    返回值:累加后的结果

const ary = [1, 2, 3, 4, 5];
// const sum = ary.reduce((num, item) => {
//   /*
//    *第一次:1 2
//    *第二次:3 3
//    *......
//    *reduce只传递一个回调函数,那么num第一次默认是第一项
//    *后续的n是上一次函数执行的处理结果
//    */
//   //console.log(num, item);
//   return num + item;
// });

// console.log(sum);

result = ary.reduce((num, item) => {
    console.log(num, item);
    return num + item;
}, 0) // return的第二个参数就是给num赋值,item从数组第一项开始遍历
console.log(result); 

     需要注意的是:第二项不传,num的值是数组中的第一项,第二项传了,num就是你传入的那个值,num除了第一次有这样的特殊性以外,后面是每遍历数组中的一项,num就是上一次执行函数的结果  

  Array.prototype.filter

    语法:var Array=arr.filter(callback [,thisArg])

    定义:filter方法创建一个新数组, 其包含通过所提供函数实现的测试的所有元素

    参数:callback:用来测试数组的每个元素的函数,调用时使用参数(element,index,array)返回true保留,false不保留。

       thisArg:可选,执行callback时的用于this的值,否则callback在非严格模式下this的值为全局对象,严格模式下为undefined.

    返回值:一个新的、由通过测试的元素组成的数组,如果没有任何数组元素通过测试,则返回空数组。不会改变原数组。

    <script>
        //找出数组中大于等于10的值
        arr = [12, 3, 45, 2, 100, 0.9];
        var new_Array = arr.filter((element) => {
            return element >= 10;
        },this);
        console.log(new_Array,this); //(3) [12, 45, 100] Window 
    </script>

  Array.prototype.sort

    语法:arr new_array=arr.sort([compareFunction])

    定义:对数组的元素做原地的排序,并返回这个数组。 sort 排序可能是不稳定的。默认按照字符串的Unicode码位点(code point)排序

    参数:compareFunction用来指定按某种顺序进行排列的函数,它里面有两个参数(firstE1:第一个用于比较的元素,secondE2:第二个用于比较的元素)。可选如果省略,元素按照转换为的字符串的诸个字符的Unicode位点进行排序。

    返回值:排序后的数组。

    <script>
        const name = ['davina', 'lisa', 'amy', 'catherine', 'lily'];
        console.log(name.sort());  //(5) ["amy", "catherine", "davina", "lily", "lisa"]

        //如果指明了compareFunction那数组会按照调用这个函数的返回值排序,比较函数格式如下:
        function compare(a, b) {
            if (a < b) { //按某种排序标准进行比较, a 小于 b
                return -1
            }
            if (a > b) {
                return 1;
            }
            //a=b
            return 0;
        }

        var numbers = [1, 2.3, 5, 0, 9, 8, 10, 34];
        const num = numbers.sort((a, b) =>
            a - b
        );
        console.log(num); // [0, 1, 2.3, 5, 8, 9, 10, 34]
    </script>

    函数作为返回值它也有很多应用场景,让函数继续返回一个可执行的函数,这意味着运算过程是可以延续的。

    <script>
        function a() {
            alert('a');
            return function () {
                alert('b');
            }
        }
        a()();
        //嵌套函数
        function fn() {
            alert('这是fn函数');
            function fn1() {
                alert('fn1')
            }
            fn1();
        }
        fn();
    </script>

  三、高阶函数的应用场景

  1. 实现AOP

    AOP(面向切面编程)的主要作用是把一些跟核心业务逻辑模块无关的功能抽离出来,这些跟业务逻辑无关的功能通常包括日志统计、安全控制、异常处理等。把这些功能抽离出来之后,再通过“动态织入”的方式掺入业务逻辑模块中。这样做的好处首先是可以保持业务逻辑模块的纯净和高内聚性,其次是可以很方便地复用日志统计等功能模块。可以通过扩展Function.prototype来实现。

  2. 函数柯里化(Currying)

    函数柯里化又称为部分求值,是把接受多个参数的函数变成接受一个单一参数的函数并且返回接受余下的参数而且返回结果的新函数的技术。简单来说就是把一个带有多个参数的函数拆分成一系列带有部分参数的函数。

function add(a, b) {
  return a + b;
}
console.log(add(2, 3)); //5  这个函数有2个参数

//currying这个函数
function add1(c) {
   /*  
    *第一次执行函数,形成一个不被释放的上下文(闭包),在闭包中我们保存下来传递的参数信息
    *后期执行其它函数时,可以基于作用域链机制,找到闭包中存储的信息,进行使用
    *类似于预先把一些信息进行存储 
    */
    return function (d) {
        //最后小函数执行时,需要把之前传递的值和最新传递的值进行累加
    return c + d;
  };
}
console.log(add1(2)(3)); //5

  currying函数的实质其实就是预先存储的概念,利用了闭包的特性来保存中间过程中输入的参数。柯里化的过程是逐步传参,逐步缩小函数的适用范围,逐步求解的过程。

    <script>
        let add = function (items) {
            return items.reduce(function (a, b) {
                return a + b
            });
        };
        console.log(add([1, 2, 3, 4]));//10
        
        //把传入的参数乖以10之后再相加
        let sum = function (items, mul) {
            return items.map(function (items) {
                return items * mul;
            }).reduce(function (c, d) {
                return c + d;
            })
        }
        console.log(sum([1, 2, 3, 4], 10)) //100
    </script>

  通常的柯里化函数:把最后的一次的计算封装进一个函数中,再把这个函数作为参数传入柯里化函数。这样做的话调用清楚明了。如果累加多值,则多值传入。

    <script>
        let currying = function (fn) {
            var args = [];
            return function () {
                  //没有参数
                if (arguments.length === 0) {
                    return fn.apply(this, args);
            //有参数
}
else { Array.prototype.push.apply(args, [].slice.call(arguments)); return arguments.callee; } } }; var mul = function () { var total = 0; for (var i = 0, c; c = arguments[i++];) { total += c; } return total; } var sum = currying(mul); sum(100)(200)(300); console.log(sum()); //600 (空白调用时才是真正的计算) </script>

  currying具有:延迟计算,参数复用,动态生成函数的作用下面来一一介绍。

    延迟计算:上面的求和可以看出

    参数复用:实现参数复用是currying的主要用途之一。下面是一个正则的验证。如果有多个地方都要检查是否有数字需要将reg参数进行多次的复用,那下面那种方法最方便 

    <script>
        function check(reg, str) {
            return reg.test(str);
        }
        console.log(check(/d+/g, 'dvaina'));  //false
        console.log(check(/[a-z]+/g, 'davina')); //true
        // currying后
        function curryingCheck(reg) {
            return function (str) {
                return reg.test(str);
            }
        }
        var num = curryingCheck(/d+/g);
        var Str = curryingCheck(/[a-z]+/g)
        console.log(num('davina')) //false
        console.log(Str('wanghao'));//true
    </script>

    反柯里化(unCurrying):从字面上我们可以看出它的意义和用法与函数柯里化相反正好相反,扩大适用范围,创建一个应用范围广的函数。下面是一个简单的实现:

    <script>
        let uncurrying = function (fn) {
            return function () {
                var args = [].slice.call(arguments, 1);
                return fn.apply(arguments[0], args);
            }
        }
        var test = 'a,b,c';
        var split = uncurrying(String.prototype.split);
        console.log(split(test, ','))
    </script>

  把uncurrying单独封装成一个函数,使用时调用 uncurrying 并传入一个现有函数fn, 反柯里化函数会返回一个新函数,该新函数接受的第一个实参将绑定为fn中this的上下文,其他参数将传递给 fn作为参数。 

  3. compose函数

   在函数式编程中有一个很重要的概念那就是函数组合,实例上就是把处理数据的函数像管道一样连接起来,然后让数据穿过管道得到最终的结果,如下面代码所示:

let add1 = (x) => x + 1;
let mul3 = (x) => x * 3;
let div2 = (x) => x / 2;
div2(mul3(add1(add1(0)))); //=>3

   但是这种写法的可读性比较差,所以就有了compose函数。compose函数是一个函数(函数只接受一个参数)的返回值作为另外一个函数的参数,将需要嵌套执行的函数平铺。它可以使得代码更加精练,代码可读性更好。

function compose(...funcs) {
    //...funcs接收的就是所有传递进来的函数
    return function anonymous(val) {
        //val第一个函数执行时需要的实参[div2,mul3,add1,add1]
        //要用到reverse()方法进行反转
        return funcs.reverse().reduce((num, item) => {
            //当没有传入函数时
            if (funcs.length === 0) return val;
            //当传入的函数为1时
            if (funcs.length === 1) return funcs[0](val);
            //num如果是一个函数那首先要把val传入num中执行,把执行的结果赋值给item
            //num不是函数就把num当成参数传递给item
            return typeof num === 'function' ? item(num(val)) : item(num);
        });
    }
}

let result = compose(div2, mul3, add1, add1)(0);
console.log(result);

 

 

原文地址:https://www.cnblogs.com/davina123/p/12023916.html