03 知识点回顾——闭包和高阶函数

写在前面的话

这个系列的文章是通过对《JavaScript设计模式》一书的学习后总结而来,刚开始觉得学习的时候只需看书即可,不用再另外记录笔记了,但是后面发现书中有些内容理解起来并不是很容易,所以结合书中的描述就将自己的理解也梳理了一下并将它记录下来,希望和大家一起学习,文章中如果有我理解错的内容,请各位批评指正,大家共同进步~

通过前两篇文章的介绍,我们对JS中的面向对象、this、call和apply这些知识点进行了介绍,今天这篇文章对闭包和高阶函数做一下介绍。很多人学习JS的时候被闭包和高阶函数折磨得不轻,在这个时候我们一定要记住编程界至高无上的一句话:如果你觉得一个概念很复杂,那么很可能是你理解错了

 

目录

写在前面的话

目录

内容

闭包的基本介绍

变量作用域

变量的生存周期

闭包的应用场景

闭包与内存管理

高阶函数介绍

函数作为参数传递

函数作为返回值输出

高阶函数实现AOP

高阶函数的其他应用

总结

 

内容

闭包的基本介绍

“闭包”这个词是JavaScript中可以搞得我们头大的东西,是我们学习JS的时候必须要面对的一道坎,也正是因为这玩意比较难搞,所以这一篇文章延迟了这么久才发布。闭包也是在我们前端开发的求职面试中屡屡被问的一个知识点,所以我们在此处花点精力来学习下闭包。

在《JS高级教程》里,对闭包是这样定义的:闭包是指有权访问另一个函数作用域中的变量的函数。所以在JS高教里将闭包定义成了一个函数,这个函数是可以访问另一个函数作用域中的变量的。但是如果你这么来理解闭包的话,那我相信你跟我一样是理解不了闭包的,或者说你只能将闭包理解成一个比较特殊的函数而已。那到底什么是闭包呢,在搞清楚这问题之前,我们先来讲解下闭包中的这个“闭”到底是怎么一回事,它“闭”的又是一个什么样的“包”呢?

其实大家在看到“闭包”这个词的时候,第一感觉就是:闭包嘛,肯定是有一个闭合的东西,然后将某一个东西或者某一片区域包裹住,最后形成一个封闭的包,那这样子就是一个闭包了啊。在这里我还告诉你了,还真不是这么个道理,其实闭包的原文是“Closure”,跟“包”没有任何的关系。所以说闭包最关键的还是这个“闭”字,在这里,我们可以将“闭”理解成封闭、关闭等。在理解了“闭”这个字的意思之后,我们返回去结合JS高级里的定义,最后结合JS的作用域特点,不难得出下面的结论:

闭包是一个比较特殊的函数,这个函数肯定是定义在另一个函数中的,换句话说它是定义在一个函数内部的函数。因为只有在函数内部才有函数作用域这个概念,要不然就是全局作用域,在JS这门语言中我们知道在局部作用域中是可以获取父级作用域中的变量的,所以我们定义的函数要想获取到另一个函数作用域中的变量,那么它就只能定义在那个函数的内部,类似于下面这样子:

        function funcA() {
            var name = "xbeichen.cn";
            function funcB() {
                return name;
            }
        }

上面代码仅仅是做一下演示,它是没有任何使用价值的,但是代码很好的描述了我们刚才对于闭包的初步理解。在上面的代码里,funcB函数定义在funcA函数里面的,它是可以获取funcA函数作用域中的变量name的,所以我们在此处就可以将funcB函数理解成一个闭包。难道闭包就这么简单嘛?那肯定不是,我们在此处只是做一个简单的理解,接下来我们将逐渐深入。

我们将以上代码中的funcB函数理解成了一个闭包,因为它获取了外部函数作用域中的变量name,但是如果我们要想在funcA函数外部使用这个变量的话,目前这段代码是不行的,因为funcB只是将name这个变量获取后返回了,但是funcB这个函数本身我们在funcA函数外部是无法访问的,所以我们在funcA函数内还需要将funcB函数返回,这样我们在外部即全局作用域下调用funcA这个函数时,定义在它内部的funcB函数也会被调用,从而达到在外部作用域中能够访问funcA内部作用域中的变量name的作用,所以完善后的代码如下所示:

        function funcA() {
            var name = "xbeichen.cn";
            function funcB() {
                return name;
            }
            return funcB;
        }

        var test = funcA();
		test();

        //或者像很多博客写的这样
        function funcA() {
            var name = "xbeichen.cn";
            return function() {        //此处的你匿名函数相当于funcB
                return name;
            }
        }

        var test = funcA();
		test();

那到此为止呢,我们的闭包就形成了,变量name和funcB函数就形成了一个闭包。通过闭包这个机制,我们可以在外部环境(在此处是全局作用域)能够访问funcA函数作用域内部的变量name了,所以我们也可以这样理解闭包:函数和函数内部能访问到的变量的总和就是一个闭包。到这一步的话,我们关于闭包的基础理解就结束了,那么你会有很多的疑问,我猜应该是以下这几个吧:

问题一:

Q:我们为什么要将变量定义在函数里,并且还要嵌套函数呢?

A:将变量定义在函数里面是为了隐藏变量,某种意义上来说是为了保护变量,同时也保护全局作用域不被污染;那么嵌套函数的目的就是为了制造一个局部变量,在我们的代码里这个局部变量就是name,所以将变量定义在函数中和嵌套函数是跟闭包没有任何关系的,我们仅仅是通过这种方法来创建了一个局部变量,并且将这个变量得到了一定程度上的保护。

问题二:

Q:在上述代码中为了让funcB函数可以在外部作用域调用,我们在funcA函数里将它返回了,如果不返回可以吗?

A:完全是可以的,除了return(返回)这个内部函数之外,你还可以通过“window.funcB = funcB”这种方式,将它定义成一个全局对象的方法,在外部环境你同样可以使用funcB这个函数,这种做法是完全没有问题的,所以这样看来return内部函数也跟闭包没有任何关系。

问题三:

Q:介绍到目前为止,我们闭包的作用看起来就是在外部环境可以访问funcA内部作用域中的变量name而已,那我直接在funcA函数中将它return就行了呀,比如下面代码这种:

        function funcA() {
            var name = "xbeichen.cn";
            return name;
        }

        var test = funcA();

上面代码所示,我照样可以在外部环境访问内部变量,为什么还要额外定义一个内部函数呢?

A:在回答这个问题之前呢,我首先承认一点的就是,到目前为止,我还没有介绍完全部关于闭包的知识,所以你才会有这个疑问,第二个原因就是你肯定没有仔细阅读我前半部分的文字,因为我前面说过,闭包是一个函数,我们使用闭包除了可以在外部环境使用函数作用域中的变量之外,也让我们的变量得到了保护,其次呢,闭包这个函数是可以操作这个变量的(后续篇幅将做介绍),所以上述代码所示的情况中是不存在闭包这个东西的。“return name”这种方式我们确实是可以获取到这个变量,但是你获取到的仅仅是name这个变量的值,并不是这个变量本身,而且我们return funcB的时候,不仅仅是返回了一个闭包函数,更重要的是返回了这个闭包函数的执行上下文,即返回了执行环境,通过一个计数器的例子我们来详细理解这句话:

计数器例子

上面代码所示,我照样可以在外部环境访问内部变量,为什么还要额外定义一个内部函数呢?

定义一个函数,然后调用这个函数,如果我们想要知道这个函数调用了多少次的话,那我们是不是可以按照如下所示的代码这样来写:

        var counter = 0;
        function funcA() {
            //do somthing
            counter ++;
            console.log(counter);
        }

        funcA();
        funcA();
        funcA();

由上述代码可看到,我们的函数正确执行,也得到了预期的结果,但是上述代码中存在一个问题,我们的counter变量是一个全局变量,这就意味着我们在代码的任何位置都可以修改它,那这样的代码就不是很理想了,所以我们要将这个变量移动到函数中来,然后让它成为一个函数内的局部变量,这样一来在全局环境下我们就无法随意修改它了,OK,代码和运行结果如下所示:

        function funcA() {
            var counter = 0;
            //do somthing
            counter ++;
            console.log(counter);
        }

        funcA();
        funcA();
        funcA();

通过修改代码可以看到,虽然在保护变量这个层面我们得到了解决,但是运行结果却违背了我们的预期,这是为什么呢?因为我们每次调用funcA这个函数的时候,此函数就会每次都被定义,相应的,我们的counter这个局部变量也会被重复定义,那不管我们运行多少次,它的值最后都是“1”。难道就没有一种做法,可以让这个函数不被重复定义,并且让它里面的变量也不被重复定义吗?这就意味着我们的这个函数和相应的函数内部变量不被我们JS的垃圾回收机制回收就可以了啊,就是说,让这个函数中的作用域常驻于内存。这么伟大的事情,就靠我们今天介绍的闭包这个机制来实现了,因为闭包除了可以让我们在外部环境访问内部函数中的变量之外,它还有一个更重要的作用,就是会保留它的执行上下文,从而不会被垃圾回收机制来清理,通过闭包修改后的代码和运行结果如下所示:

        function funcA() {
            var counter = 0;
            //do somthing
            function funcB() {
                counter ++;
                console.log(counter);
            }
            return funcB;
            
        }

        var test = funcA();
        test();
        test();
        test();

由上可看出,我们运用闭包这个机制之后,得到了我们想要的结果,同时也保护了我们的变量。那这个过程是如何实现的呢(这一点我在前面的文字中是没有写到的)?这是因为闭包函数在访问我们的内部变量counter,所以JS中的垃圾回收机制监测到counter变量的时候,发现它一直是被我们的闭包函数使用的,也就是说这个变量永远不会被贴上“清理”的标记,这样一来我们的counter变量是长期存活在我们的内存中的,所以这就是我们可以在外部运用闭包函数操作counter变量时发现它还保留着上次运行结果的重要原因。所以说,计数器这个例子,很好的解释了闭包以及它的典型使用场景,但这并不是闭包唯一的使用场景,这篇文章接下来会介绍更多关于闭包的知识。但到目前为止,关于闭包的介绍性内容就结束了,我们来做一个小总结:

一个闭包由两部分组成:函数和创建该函数的环境。这个环境是由环境中的局部变量组成的。对于闭包test来说,它由函数funcB和变量counter组成,所以这时候test是可以访问变量counter的。当我们将funcB赋值给变量test的时候,一个完整的闭包就出现了。它阻止了JS垃圾回收机制对函数内部变量的回收,导致函数内部变量的引用计数一直不为0,所以它无法被垃圾回收器回收。所以我们的闭包严格来说应该满足以下三个条件:

  • 访问所在作用域
  • 函数嵌套
  • 在所在作用域外被调用

通过以上的理解,JS中的闭包我们不能很局限的理解成一个特殊的函数了,它确切点说应该是JS中的一种机制,通过这种机制,我们可以完成更多有趣的事情。

以上篇幅中介绍闭包的时候常常提到作用域这些,看来闭包跟作用域有着密切的联系,所以接下来我们看看作用域有关的知识。

变量作用域

变量作用域就是变量的有效范围,比如上文中所说的name这个变量,它的作用范围就是funcA函数内部,在函数外部是不能直接访问和操作的。在JS中我们一般是通过函数来创建一个局部作用域,也可以称作函数作用域,如果在这个函数作用域中声明了一个变量的话,它就是一个局部变量,从而可以达到保护变量和避免全局作用域被污染的情况发生。此外,如果在函数作用域中声明变量时,不加var关键字的话我们声明的其实是一个全局作用域变量,只有加上了var这个关键字,这个变量才是函数作用域中的内部变量,如下代码所示:

        var website1 = 'xbeichen.cn';             //全局变量
        function funcA() {
            website2 = 'www.xbeichen.cn';         //声明时没有用var关键字,也是一个全局变量
            var website3 = 'geov.online';         //局部变量

            console.log('函数内部执行的输出语句(website1):' + website1);
            console.log('函数内部执行的输出语句(website2):' + website2);
            console.log('函数内部执行的输出语句(website3):' + website3);
        }

        funcA();

        console.log('函数外部执行的输出语句(website1):' + website1);
        console.log('函数外部执行的输出语句(website2):' + website2);
        console.log('函数外部执行的输出语句(website3):' + website3);

如上述代码所示,我们创建了一个funcA的函数作用域,并且在里面声明了两个变量,但是由于website2变量在声明时没有用var关键字,所以它实际上是一个全局变量,我们在函数外部照样可以访问这个变量,但是website3变量声明时用了var关键字,所以它是一个函数内部的局部变量,我们只能在函数内部使用它,在函数外部是无法使用和访问的。其实函数作用域就像一个单方向的透视镜,我们在函数内部可以访问函数外部的变量,但是在外部是无法访问函数内部的变量的。因为函数在内部寻找一个变量的时候,先从它的作用域中搜索,如果找不到的话就会沿着作用域链逐层向外寻找,直到搜索到全局作用域为止,变量的这个搜索过程从始至终都是由内向外搜索的,并不是由外向内。

变量的生存周期

除了变量的作用域之外,我们提到闭包的时候还提到了变量的生存周期。变量的生存周期其实就是变量的有效期。如果我们将这个变量定义在了全局环境中,那么它的生存周期就是永久的,除非我们手动去销毁这个变量;如果我们在函数作用域中用var关键字定义的变量,那它的生存周期就只有在函数执行时才有效,当函数执行完毕退出后这个变量也会随之被销毁,就像下面这个例子:

        function funcB() {
            var counter = 0;
            counter ++;
            console.log(counter);
        }

        funcB();
        funcB();
        funcB();

由上述代码可看到,我们在函数中定义了一个计数的变量,我想每次记录函数被调用的次数,但是最后结果始终是1,这就是因为counter这个变量是一个局部变量,当函数funcB每次执行完毕后它会随着函数环境的销毁而被销毁,所以不管我们调用多少次函数,它的值始终是1。

那学过闭包之后我们就可以用闭包来解决这个问题,修改后的代码如下所示:

        function funcB() {
            var counter = 0;
            return function() {
                counter ++;
                console.log(counter);
            }
        }

        var funcC = funcB();
        funcC();
        funcC();
        funcC();

通过使用闭包后发现,变量counter在函数执行完后并没有被销毁,而是一直保留在内存中,所以我们每次调用函数时都可以访问到counter变量上一次的值。这是因为当执行“var funcC = funcB()”时,funcC返回了一个匿名函数的引用,它可以访问到funcB被调用时产生的环境,而局部变量counter一直处在这个环境里。既然局部变量所在的环境还能被外界访问,这个局部变量就有了不被销毁的理由。在这里产生了一个闭包结构,所以局部变量的生命看起来被延续了。以上部分就是关于闭包和与闭包有牵扯的变量作用域、变量生存周期的介绍,接下来我们看看闭包的一些典型应用场景。

闭包的应用场景

1、封装变量

为了在编程开发时避免出现全局环境被污染的情况发生,我们通常会将一些变量定义在局部作用域中,也就是说将它定义在函数作用域内,这样我们的全局环境看起来是干净的,并且在全局环境下也是无法操作这个局部变量的。此时为了能够操作和访问这个局部变量,我们就需要借助闭包这个机制来进行,代码如下:

        var website = function() {          //定义一个立即执行函数
            var name = "xbeichen.cn";
            return {
                getName: function() {          
                    return name;
                },
                setName: function(nameValue) {
                    name = nameValue;
                }
            }
        }();

        console.log(website.name);
        console.log(website.getName());
        console.log(website.setName("geov.online"));
        console.log(website.getName());

可以看到,我们直接在全局环境是无法访问到name这个变量的,只有通过闭包机制定义两个函数来获取和修改这个变量的值。以上代码中是有两个闭包的,变量name和getName函数组成了一个闭包;变量name和setName也组成了一个闭包,所以大家可以看到闭包无处不在。

2、延续变量的生命周期

上文中计数器的例子已经很好的解释了闭包可以延续变量生命周期的这个作用,此处我们不再做详细介绍。

3、闭包与面向对象

在JS中通常用面向对象实现的功能,用我们的闭包也是可以实现的,同样的,用闭包能实现的功能也可以用面向对象的思想来实现。因为我们JS的祖先语言Scheme语言中甚至是没有提供面向对象这个原生设计的,但是我们依然可以用闭包的思想来实现一个完整的面向对象系统。这中间涉及到的代码示例其实我们在第一个场景中已经做介绍了,下面再来看一下:

        var website = function() {
            var name = "xbeichen.cn";
            return {
                getName: function() {
                    return name;
                },
                setName: function(nameValue) {
                    name = nameValue;
                }
            }
        }();

        console.log('闭包形式: ' + website.name);
        console.log('闭包形式: ' + website.getName());
        console.log('闭包形式: ' + website.setName("geov.online"));
        console.log('闭包形式: ' + website.getName());

        console.log('我可是分割线——————————————————————————')
        //换成面向对象的写法
        var website2 = {
            name: 'geov.online',
            getName: function() {
                return this.name;
            },
            setName: function(nameValue) {
                this.name = nameValue;
            }
        }

        console.log('面向对象形式: ' + website2.name);
        console.log('面向对象形式: ' + website2.getName());
        console.log('面向对象形式: ' + website2.setName("xbeichen.cn"));
        console.log('面向对象形式: ' + website2.getName());

以上的两部分代码中,第一部分是用了一个立即执行函数,然后结合闭包机制来实现了一个website对象;第二部分代码直接用JS的对象写法,实现了一个website2对象。最后在控制台我们可以看到,这两种实现思路都是可以互换的。

以上三种就是闭包最常用的场景,在我们的JS开发中闭包无处不在,所以它的运用场景非常多,在后续文章中我们遇到的话继续学习,这篇文章对于闭包的运用场景就暂时介绍到这里。

闭包与内存管理

在我们查找闭包的很多资料文档时,这些文档都会提到一个问题,就是说我们的闭包会造成内存泄漏,就像我们刚开始介绍闭包时候的代码或者介绍闭包典型应用场景的时候所用的代码,只要我们使用闭包,那么闭包引用的这个变量就会一直存在于内存,不会被垃圾回收机制回收,这样一看确实闭包会造成内存泄漏,但是我们仔细想想,如果我们不使用闭包,将这个局部变量定义成全局变量,那么它也会常驻于内存呀,所以造成内存泄漏不是闭包的问题,而是我们垃圾回收机制的问题,更加确切地说是浏览器的问题,在更加确切点说是IE的问题,详情请看这篇文章("https://www.cnblogs.com/rubylouvre/p/3345294.html")。

高阶函数介绍

只要是满足这两个条件之一的函数我们都可以称之为高阶函数:函数可以作为参数被传递、函数可以作为返回值输出。那对应到我们JS的函数时发现,JS中的函数是满足高阶函数的条件的,下面我们就来详细介绍一下。

函数作为参数传递

函数作为参数传递这意味着我们可以分离我们的代码,将可变的部分作为函数参数来进行传递,不变的部分作为接受参数的另一个函数体,这种典型的应用在以下两个场景中最常见:

1、回调函数

回调函数在我们的ajax中是使用最频繁的,因为ajax的异步请求中我们并不知道它什么时候执行完成,所以我们只能在ajax的方法中传入一个参数,这个参数就是回调函数,它知道请求什么时候完成,所以我们将请求完成后要进行的操作封装到这个回调函数里即可,实例代码如下:

        var getData = function(nameID, callback) {
            $.ajax('http://www.xbeichen.cn?' + nameID, function(result) {
                if(typeof callback === 'function') {
                    callback(result);
                }
            });
        }

        getData('xuqw', function(data) {
            console.log(data);
        })

以上是我们的高阶函数在回调函数这种场景中的应用。其实除了回调函数中的应用之外,当我们遇到一些需求飘忽不定或者后期变化频繁的情况下,我们可以将这部分变化的逻辑进行抽离和封装,使用高阶函数来分离我们需求中可变和不变的部分,这也是我们用高阶函数可以解决的问题。

2、Array.prototype.sort

数组的sort方法是用来排序的,但是用正序还是逆序,这就要我们为sort这个方法传入一个函数参数来决定了,代码如下:

        var dataArray = [1,2,4,2,4,6,8];

        //正序
        dataArray.sort( function(a, b) {
            return a-b;
        });

        console.log(dataArray);

        //逆序
        dataArray.sort( function(a, b) {
            return b-a;
        });

        console.log(dataArray);

函数作为返回值输出

函数作为返回值输出最典型的就是闭包的代码中,我们一般是将内部函数作为返回值返回,此处就不再做过多的陈述。后面我们会遇到更多将函数作为返回值输出的情景,到时候一起介绍。

高阶函数实现AOP

AOP全称是面向切面编程,它的核心作用就是将我们系统代码中的一些跟核心业务逻辑无关的功能抽离出来,将它们再所需要的地方“动态织入”,这样做的好处就是能保证业务逻辑代码的纯净和抽离出来的这部分模块的可复用性。接下来的代码中我们通过扩展Function来做演示,代码如下:

        Function.prototype.before = function(before) {
            var _self = this;
            return function() {
                before.apply(this, arguments);
                return _self.apply(this, arguments);
            }
        }

        Function.prototype.after = function(after) {
            var _self = this;
            return function() {
                var ret = _self.apply(this, arguments);
                after.apply(this, arguments);
                return ret;
            }
        }

        var func = function() {
            console.log(2);
        }

        func = func.before(function(){
            console.log(1);
        }).after(function(){
            console.log(3);
        })

        func();

高阶函数的其他应用

1、函数柯里化(缩小适用范围,创建针对性更强的函数)

柯里化又称为部分求值,如果一个函数是柯里化函数,那么它会接受一些参数,但是接受这些参数之后它并不会立即求值,而是继续返回一个函数,刚才传入的参数和这个返回的函数之间形成闭包被保存起来,等到我们的函数真正需要求值的时候,之前传入的参数才会被一次性用于求值,我们看下面的代码来理解这个过程:

        //原始函数
        function sum(a, b, c) {
            return a + b + c;
        }

        console.log('原始函数:' + sum(4,5,6));

        //柯里化后的函数
        function sum2(a) {
            return function(b) {
                return function(c) {
                    return a + b + c;
                }
            }
        }

        var sum01 = sum2(4);
        var sum02 = sum01(5);
        console.log('柯里化函数:' + sum02(6));

上面代码第一部分我们定义了一个原始函数sum,用于求三个值的和;第二部分我们将这个原始函数进行了柯里化,首先是调用sum2(4),此时变量sum01相当于

			var sum01 = function(b) {
                return function(c) {
                    return a + b + c;
                }
            }

然后调用sum01(5),此时的变量sum02相当于

				var sum02 = function(c) {
                    return a + b + c;
                }

最后调用sum02(6),返回a+b+c的结果。这是一个最简单的函数柯里化过程。其实所谓的函数柯里化就是将具有很多参数的函数转换成具有较少参数的函数的过程。大家看完函数柯里化这个例子之后不禁会有疑问,函数柯里化后虽然我们的参数是少了,但是它的代码量却增加了不少,这不是额外增加了负担吗,代码看起来很臃肿,那么这个函数柯里化有什么用呢?

上述这个最简单的函数柯里化实例确实是存在这个问题的,我们通过将原函数柯里化后,代码量增加了不少。但是函数柯里化在很多场景下是很有用的,或者说我们在平时不经意间已经使用了函数柯里化了,接下来这个实例就给大家展示一下函数柯里化真正的用途:

        //原始函数,计算长方体体积
        function volume(length, width, height) {
            return length * width * height;
        }

        console.log(volume(100,200,300));
        console.log(volume(100,120,100));
        console.log(volume(100,400,20));
        console.log(volume(230,120,109));

以上代码中的函数是关于计算长方体体积的函数,我们通过传入长、宽、高三个参数后,我们的函数会返回相应的体积值。在上面的求值过程中我们发现其实有很多长方体是长度相同的,所以这时候我们就可以用函数柯里化来将我们原始的函数进行优化一下:

        //优化后的函数,计算长方体体积
        function volume2(length, width, height) {
            return function(width) {
                return function(height) {
                    return length * width * height;
                }
            }
        }

        var volumeLength100 = volume2(100);
        console.log(volumeLength100(200)(300));
        console.log(volumeLength100(120)(100));
        console.log(volumeLength100(400)(20));
        console.log(volume2(230)(120)(109));

通过以上优化,我们将长度为100的长方体进行了统一处理,这样就实现了我们的参数复用。在此大家可以看到函数柯里化是很实用的,那么我们能不能有一个通用的柯里化函数呢,就是说这个函数可以把函数参数转换成柯里化函数,接下来我们实现一下这样的函数:

        // 柯里化通用式 ES5
        function currying(func, args) {
            // 形参个数
            var arity = func.length;
            // 上一次传入的参数
            var args = args || [];

            return function () {
                // 将参数转化为数组
                var _args = [].slice.call(arguments);

                // 将上次的参数与当前参数进行组合并修正传参顺序
                Array.prototype.unshift.apply(_args, args);

                // 如果参数不够,返回闭包函数继续收集参数
                if(_args.length < arity) {
                    return currying.call(null, func, _args);
                }

                // 参数够了则直接执行被转化的函数
                return func.apply(null, _args);
            }
        }

上面的代码使用call和apply的方式实现了一个通用的柯里化转换函数,利用闭包将函数的参数先存储起来,等到参数达到一定数量之后再执行函数。所以大家也可以看到,函数柯里化是以闭包和高阶函数为基础的。

2、反柯里化(扩大适用性)

反柯里化的思想和柯里化正好相反,柯里化是将函数拆分成功能更具体的函数,反柯里化则是扩大函数的适用性,它可以做到将原本作为特定对象的特定方法给任意对象使用。

反柯里化通用式的参数为一个希望可以被其他对象调用的方法或函数,通过调用通用式返回一个函数,这个函数的第一个参数为要执行方法的对象,后面的参数为执行这个方法时需要传递的参数,代码如下:

// 反柯里化通用式 ES5
function uncurring(fn) {
    return function () {
        // 取出要执行 fn 方法的对象,同时从 arguments 中删除
        var obj = [].shift.call(arguments);
        return fn.apply(obj, arguments);
    }
}

3、函数节流

在JS中的函数通常情况下都是由用户主动触发然后进行调用的,这些函数如果不是逻辑实现特别不合理的话,一般不会遇到性能问题。但是在某些情况下的一些函数不是由用户主动触发的,这些情况下我们的函数会被频繁地调用,比如以下这几种情况:

1)window.onresize方法。在我们的浏览器窗口大小改变的时候,这个函数会被自动触发,如果我们在这个方法中涉及到关于DOM的操作,那我们频繁改变窗口大小的话会严重影响浏览器性能,很有可能最后会造成浏览器卡顿;

2)mouseover方法。当我们鼠标拖动的时候也会频繁触发这个方法,此方法中如果涉及DOM操作,也会影响性能;

3)上传进度。有些上传功能会使用相关的浏览器插件,这些插件在文件上传之前会对文件进行扫描并随时通知JS函数,通知的频率一般是一秒钟十次左右,显然这样也会影响我们浏览器性能。

以上三种情况是在我们开发时经常遇到的情况,它们最核心的问题就是函数被触发的频率太高,为了解决这个问题,我们就需要限制它的触发次数。比如窗口大小拖动过程中可能在一秒内会触发resize函数十多次,那我们想要的其实仅仅需要触发一两次就可以,这种需求最简单的就是通过setTimeout来实现,下面是实例代码:

function throttle(fn, delay) {
    var timer;
    return function () {
        var _this = this;
        var args = arguments;
        if (timer) {
            return;
        }
        timer = setTimeout(function () {
            fn.apply(_this, args);
            timer = null; // 在delay后执行完fn之后清空timer,此时timer为假,throttle触发可以进入计时器
        }, delay)
    }
}

//测试上述代码
function testThrottle(e, content) {
    console.log(e, content);
}
var testThrottleFn = throttle(testThrottle, 2000); // 节流函数
window.resize = function (e) {
    testThrottleFn(e, 'throttle'); // 给节流函数传参
}

4、分时函数

在我们开发中可能会遇到这样的需求,页面初始化的时候,某一个面板中可能一次性需要渲染1000个左右的列表。在短时间内往页面中添加大量DOM节点的时候,我们的浏览器是吃不消的,这就会造成前端页面的卡顿,为了解决这个问题,我们需要分时函数来处理。分时函数的核心思想就是将创建节点的这个工作分批进行,例如将一秒内创建1000个列表改为每200毫秒创建8个节点这样来操作,实例代码如下:

        //分时函数
        var timeChunk = function( ary, fn, count ){
            var obj,t;
            var len = ary.length;
            var start = function(){
                for ( var i = 0; i < Math.min( count || 1, ary.length ); i++ ){
                    var obj = ary.shift();
                    fn( obj );
                }
            };
            return function(){
                t = setInterval(function(){
                    if ( ary.length === 0 ){ // 如果全部节点都已经被创建好
                        return clearInterval( t );
                    }
                    start();
                }, 200 ); // 分批执行的时间间隔,也可以用参数的形式传入
            };
        };

5、惰性加载函数

在 Web 开发中,因为浏览器之间的实现差异,一些嗅探工作总是不可避免。比如我们需要一个在各个浏览器中能够通用的事件绑定函数 addEvent ,常见的写法如下:

var addEvent = function( elem, type, handler ){
    if ( window.addEventListener ){
        return elem.addEventListener( type, handler, false );
    }
    if ( window.attachEvent ){
        return elem.attachEvent( 'on' + type, handler );
    }
};

这个函数的缺点是,当它每次被调用的时候都会执行里面的 if 条件分支,虽然执行这些 if分支的开销不算大,但也许有一些方法可以让程序避免这些重复的执行过程。

第二种方案是这样,我们把嗅探浏览器的操作提前到代码加载的时候,在代码加载的时候就立刻进行一次判断,以便让 addEvent 返回一个包裹了正确逻辑的函数。代码如下:

var addEvent = (function(){
    if ( window.addEventListener ){
        return function( elem, type, handler ){
            elem.addEventListener( type, handler, false );
        }
    }
    if ( window.attachEvent ){
        return function( elem, type, handler ){
elem.attachEvent( 'on' + type, handler );
}
    }
})();

目前的 addEvent 函数依然有个缺点,也许我们从头到尾都没有使用过 addEvent 函数,这样看来,前一次的浏览器嗅探就是完全多余的操作,而且这也会稍稍延长页面 ready的时间。

第三种方案即是我们将要讨论的惰性载入函数方案。此时 addEvent 依然被声明为一个普通函数,在函数里依然有一些分支判断。但是在第一次进入条件分支之后,在函数内部会重写这个函数,重写之后的函数就是我们期望的 addEvent 函数,在下一次进入 addEvent 函数的时候, addEvent函数里不再存在条件分支语句:

<body>
    <div id="div1">点我绑定事件</div>
    <script>
        var addEvent = function( elem, type, handler ){
            if ( window.addEventListener ){
                addEvent = function( elem, type, handler ){
                    elem.addEventListener( type, handler, false );
                }
            }else if ( window.attachEvent ){
                addEvent = function( elem, type, handler ){
                    elem.attachEvent( 'on' + type, handler );
                }
            }
            addEvent( elem, type, handler );
        };

        var div = document.getElementById( 'div1' );
        addEvent( div, 'click', function(){
            alert (1);
        });
        addEvent( div, 'click', function(){
            alert (2);
        });
    </script>
</body>

以上就是我们高阶函数常用的五种典型场景。到此为止,我们关于闭包和高阶函数的基本介绍就结束了。

 

总结

这篇文章是在我们开始学习JS设计模式之前的基础知识回顾系列中最后一篇文章,本文对JS里面的闭包和高阶函数做了一下简单的介绍。通过前面两篇文章和本文,我们已经对JS中的面向对象、this、call、apply、闭包和高阶函数都做了知识回顾,接下来我们就进入正式的设计模式的学习。

 
评论 10
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

X北辰北

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值