美文网首页Web前端之路让前端飞我爱编程
闭包详解二:JavaScript中的高阶函数

闭包详解二:JavaScript中的高阶函数

作者: iceman_dev | 来源:发表于2018-06-05 19:59 被阅读50次

    本文讲解的高阶函数是之前讲解的闭包的续集,所以在学习高阶函数之前,一定要确保对闭包以及作用域的概念已经有了解:

    理解抽象

    引出抽象的概念

    有Java、C#等开发经验的同学对代码抽象的思想一定不会陌生,抽象类、接口平时写的非常多,但是对于一直都从事前端开发的同学来说,“抽象”这个词就比较陌生了,毕竟JavaScript中没有abstract、interface。

    但是JS中肯定是有代码抽象的思想的,只不过是形式上和Java等语言不同罢了!

    先来看Java中的一个抽象类:

    public abstract class SuperClass {
      public abstract void doSomething();
    }
    

    这是Java中的一个类,类里面有一个抽象方法doSomething,现在不知道子类中要doSomething方法做什么,所以将该方法定义为抽象方法,具体的逻辑让子类自己去实现。

    创建子类去实现SuperClass:

    public class SubClass  extends SuperClass{
      public void doSomething() {
        System.out.println("say hello");
      }
    }
    

    SubClass中的doSomething输出字符串“say hello”,其他的子类会有其他的实现,这就是Java中的抽象类与实现。

    那么JS中的抽象是怎么样的,最为经典的就是回调函数了:

    function createDiv(callback) {
      let div = document.createElement('div');
      document.body.appendChild(div);
      if (typeof callback === 'function') {
        callback(div);
      }
    }
    createDiv(function (div) {
      div.style.color = 'red';
    })
    

    这个例子中,有一个createDiv这个函数,这个函数负责创建一个div并添加到页面中,但是之后要再怎么操作这个div,createDiv这个函数就不知道,所以把权限交给调用createDiv函数的人,让调用者决定接下来的操作,就通过回调的方式将div给调用者。

    这也是体现出了抽象,既然不知道div接下来的操作,那么就直接给调用者,让调用者去实现。 和Java中抽象类中的抽象方法的思想是一样的。

    总结一下抽象的概念:抽象就是隐藏更具体的实现细节,从更高的层次看待我们要解决的问题

    数组中的遍历抽象

    在编程的时候,并不是所有功能都是现成的,比如上面例子中,可以创建好几个div,对每个div的处理都可能不一样,需要对未知的操作做抽象,预留操作的入口,作为一名程序员,我们需要具备这种在恰当的时候将代码抽象的思想。

    接下来看一下ES5中提供的几个数组操作方法,可以更深入的理解抽象的思想,ES5之前遍历数组的方式是:

    var arr = [1, 2, 3, 4, 5];
    for (var i = 0; i < arr.length; i++) {
      var item = arr[i];
      console.log(item);
    }
    

    仔细看一下,这段代码中用for,然后按顺序取值,有没有觉得如此操作有些不够优雅,为出现错误留下了隐患,比如把length写错了,一不小心复用了i。既然这样,能不能抽取一个函数出来呢?最重要的一点,我们要的只是数组中的每一个值,然后操作这个值,那么就可以把遍历的过程隐藏起来:

    function forEach(arr, callback) {
      for (var i = 0; i < arr.length; i++) {
        var item = arr[i];
        callback(item);
      }
    }
    forEach(arr, function (item) {
      console.log(item);
    });
    

    以上的forEach方法就将遍历的细节隐藏起来的了,把用户想要操作的item返回出来,在callback还可以将i、arr本身返回:callback(item, i, arr)

    JS原生提供的forEach方法就是这样的:

    arr.forEach(function (item) {
      console.log(item);
    });
    

    跟forEach同族的方法还有map、some、every等。思想都是一样的,通过这种抽象的方式可以让使用者更方便,同事又让代码变得更加清晰。

    抽象是一种很重要的思想,让可以让代码变得更加优雅,并且操作起来更方便。在高阶函数中也是使用了抽象的思想,所以学习高阶函数得先了解抽象的思想

    高阶函数

    什么是高阶函数

    至少满足以下条件的中的一个,就是高阶函数:

    • 将其他函数作为参数传递

    • 将函数作为返回值

    简单来说,就是一个函数可以操作其他函数,将其他函数作为参数或将函数作为返回值。我相信,写过JS代码的同学对这个概念都是很容易理解的,因为在JS中函数就是一个普通的值,可以被传递,可以被返回。

    参数可以被传递,可以被返回,对Java等语言开发的同学理解起来可能会稍微麻烦一些,因为Java语言没有那么的灵活,不过Java8的lambda大概就是这意思;

    函数作为参数传递

    函数作为参数传递就是我们上面提到的回调函数,回调函数在异步请求中用的非常多,使用者想要在请求成功后利用请求回来的数据做一些操作,但是又不知道请求什么时候结束。

    用jQuery来发一个Ajax请求:

    function getDetailData(id, callback) {
      $.ajax('http://xxxxyyy.com/getDetailData?' + id, function (res) {
        if (typeof callback === 'function') {
          callback(res);
        }
      });
    }
    getDetailData('78667', function (res) {
      // do some thing
    });
    

    类似Ajax这种操作非常适合用回调去做,当一个函数里不适合执行一些具体的操作,或者说不知道要怎么操作时,可以将相应的数据传递给另一个函数,让另一个函数来执行,而这个函数就是传递进来的回调函数。

    另一个典型的例子就是数组排序

    函数作为值返回

    在判断数据类型的时候最常用的是typeof,但是typeof有一定的局限性,比如:

    console.log(typeof []); // 输出object
    console.log(typeof {}); // 输出object
    

    判断数组和对象都是输出object,如果想要更细致的判断应该要使用Object.prototype.toString

    console.log(Object.prototype.toString.call([])); // 输出[object Array]
    console.log(Object.prototype.toString.call({})); // 输出[object Object]
    

    基于此,我们可以写出判断对象、数组、数字的方法:

    function isObject(obj) {
      return Object.prototype.toString.call(obj) === '[object Object]';
    }
    function isArray(arr) {
      return Object.prototype.toString.call(arr) === '[object Array]';
    }
    function isNumber(number) {
      return Object.prototype.toString.call(number) === '[object Number]';
    }
    

    我们发现这三个方法太像了,可以做一些抽取:

    function isType(type) {
      return function (obj) {
        return Object.prototype.toString.call(obj) === '[object ' + type + ']';
      }
    }
    var isArray = isType('Array');
    console.log(isArray([1,2]));
    

    这个isType方法就是高阶函数,该函数返回了一个函数,并且利用闭包,将代码变得优雅。

    高阶函数的应用

    lodash中的使用

    高阶函数在平时的开发中用的非常多,只是有时候你不知道你的这种用法就是高阶函数,在一些开源的类库中也用的很多,比如很有名的 lodash,挑其中一个before函数:

    function before(n, func) {
      let result
      if (typeof func != 'function') {
        throw new TypeError('Expected a function')
      }
      return function(...args) {
        if (--n > 0) {
          result = func.apply(this, args)
        }
        if (n <= 1) {
          func = undefined
        }
        return result
      }
    }
    

    在before函数中,同时有用到将函数当做传递进来,又返回了一个函数,这是一个很经典的高阶函数的例子。

    看一下该代码可以怎么用吧:

    jQuery(element).on('click', before(5, addContactToList))
    

    所以before函数就是让某个方法最多调用n次。

    :before函数代码不难,使用也不难,但就是这么一个简单的工具方法需要了解的知识点有:作用域、闭包、高阶函数,所以说知识点都是连贯的,接下来要写的JavaScript设计模式系列,同样也要用到这些知识。

    函数节流

    在写代码的时候,大多数情况都是由我们自己主动去调用函数。不过在有一些情况下,函数的调用不是由用户直接控制的,在这种情况下,函数有可能被废除频繁的调用,从而造成性能问题。

    Element-UI 中,有一个 el-autocomplete 组件,该组件可以在用户输入的时候在输入框下方列出相关输入项:

    autocomplete.png

    其实就是可以在用户输入的时候,可以用已经输入的内容做搜索,饿了么在实现该组件的时候是利用input组件,并且监听用户的输入:

    实现autocomplete.png

    用input事件去监听用户输入的话,用户输入的每一个字都会触发该方法,如果是要用输入的内容去做网络搜索,用户输入的每一字都搜索的话,触发的频率太高了,性能消耗就有点大了,而且在网络比较差的情况下用户体验也比较不好。

    饿了么实现该组件的时候当然也考虑到了这些问题,用的是业界比较通用的做法→节流,就是当输入后,延迟一段时间再去执行搜索,如果该次延迟执行还没有完成的话,就忽略接下来搜索的请求。

    看一下其实现:


    实现节流.png

    autocomplete的节流思想就是刚才说的那种,并且用了 throttle-debounce 这个工具库,其实现就是利用高阶函数,有兴趣的同学可以看它的源码:https://github.com/niksy/throttle-debounce,代码并不复杂。

    高阶函数还有其他的用法,比如用在设计模式中等,这些内容将会在后面详细介绍。

    特别注意

    可以关注我的公众号:icemanFE,接下来持续更新技术文章!

    公众号.png

    相关文章

      网友评论

        本文标题:闭包详解二:JavaScript中的高阶函数

        本文链接:https://www.haomeiwen.com/subject/ymussftx.html