zoukankan      html  css  js  c++  java
  • 收了闭包这个小妖精

    前言

    说到闭包,实在是居家旅行破境渡劫摄魄迷魂必备良药!不吃不知道,一吃哇哇叫,下面我们也去搞两盒试试。

    一、闭包是什么

    闭包,一个近乎神话的概念,从字面上理解感觉就像是一个比较封闭的东西,百度百科上的定义是:闭包就是能够读取其他函数内部变量的函数。

    而我个人比较倾向于这么理解:闭包就是一个封闭包裹了它所能使用的作用域的函数。

    这样看起来好像有点那个意思了,通俗的说就是:函数这个袋子把一些作用域装起来了,哪些作用域呢?这个函数作用域链上的作用域。

    光说不写假帅气,下面来些例子瞧瞧:

    1.1 函数传递

    // 1.函数作为返回值
    function foo() { 
        var a = 2; 
        function bar() {  
            console.log( a ); 
        } 
        return bar; 
    } 
    
    var f = foo(); 
    f();   // 2  这就是闭包的效果,或者说f即bar函数就是一个闭包,它把a所在的作用域包了起来,以便自己随时使用
    

    上面的例子是将函数作为值返回,下面我们换个方式试试(其实无论使用何种方式对函数类型的值进行传递,当函数在别处被调用时都可以观察到闭包)。

    // 2.函数作为参数传递
    function foo() { 
        var a = 2; 
        function bar() { 
            console.log( a );
        } 
        f(bar); 
    } 
    
    function f(fn) { 
        fn();  // 函数作为参数传递,也包裹了a的作用域,这也是闭包
    }
    
    foo();  // 2
    
    // 3.间接传递函数
    var fn; 
    function foo() { 
        var a = 2; 
        function bar() { 
            console.log( a ); 
        } 
        fn = bar; // 将bar分配给全局变量fn
    } 
    
    function f() { 
        fn(); // fn指向bar,bar包裹着a的作用域,这也是闭包
    } 
    
    foo(); 
    f(); // 2
    
    // 4.回调函数,传递给JS引擎调用
    function wait(message) { 
        setTimeout(function timer() { 
            console.log(message); 
        }, 1000); 
    } 
    
    wait( "Hello" );  // 'Hello'
    // 将一个内部函数timer传递给setTimeout,timer具有涵盖wait作用域的闭包,因此还有对变量message的引用
    

    其实,在定时器、事件监听器、Ajax请求、跨窗口通信、Web Workers或者任何其他的异步(或者同步)任务中,只要使用了回调函数,实际上就是在使用闭包。

    所以无论通过何种手段将内部函数传递到所在的词法作用域以外,它都会持有对原始定义作用域的引用(包裹),无论在何处执行这个函数都会使用闭包。

    tip: 词法作用域指由书写代码时变量所在的位置所决定的作用域。

    1.2 IIFE

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

    以上这个立即执行函数是闭包吗?嗯,看起来应该是。

    但严格来讲它并不是闭包。为什么?因为上面的函数并不是在它本身的词法作用域以外执行的,它在定义时所在的作用域中执行,a是通过普通的词法作用域查找而非闭包被发现的。

    尽管IIFE本身并不是观察闭包的恰当例子,但它的确创建了闭包,并且也是最常用来创建可以被封闭起来的闭包的工具,后面我们会讲到。

    1.3 循环与闭包

    说到这个循环闭包的例子,可谓是如影随形,惺惺相惜,让猿欲罢不能。

    for (var i=1; i<=5; i++) { 
        setTimeout(function timer() { 
            console.log(i); 
        }, i*1000); 
    }
    

    这个想必大家伙就算没吃过也见过这个猪是怎么跑的:以每秒一次的频率输出五次6,而不是每秒一次一个的分别输出1~5。

    首先解释6是从哪里来的:这个循环的终止条件是i不再<=5,条件首次成立时i的值是6。因此,输出显示的是循环结束时i的最终值。

    仔细想一下,这好像又是显而易见的,延迟函数的回调会在循环结束时才执行。但事实上,当定时器运行时即使每个迭代中执行的是setTimeout(.., 0),所有的回调函数依然是在循环结束后才会被执行,因此会每次输出一个6出来。

    究竟是什么原因导致这结果和我们预想的不一样呢?

    原因是我们试图假设循环中的每个迭代在运行时都会给自己“捕获”一个i的副本。但是根据作用域的工作原理,实际情况是尽管循环中的五个函数是在各个迭代中分别定义的,但是它们都被封闭在一个共享的全局作用域中,因此实际上只有一个i,所以都是在共享同一个i。

    如何解决这个问题?

    我们设想一下如果每次循环函数都能将属于自己的i包裹起来,然后保存下来,那就需要闭包作用域,下面我们试试:

    for (var i=1; i<=5; i++) { 
        (function() { 
            setTimeout(function timer() { 
                console.log( i ); 
            }, i*1000); 
        })(); 
    }
    

    这样行吗?答案是不行。为什么?上面的确创建了五个封闭的作用域,但大家有没有注意到,但这个作用域是空的,它们并没有将i包裹并存储起来,我们依旧是引用外部的同一个全局i,所以这个封闭的作用域需要有自己的变量,用来在每个迭代中储存i的值:

    for (var i=1; i<=5; i++) { 
        (function() { 
            var j = i;   // 将i的值存储在闭包内
            setTimeout(function timer() { 
                console.log(j); 
            }, j*1000); 
        })(); 
    }
    

    搞定!将timer传递给setTimeout,时间到后,JS引擎会调用timer函数,然后找到对应包裹起来的i,我们还可以再改进一下:

    for (var i=1; i<=5; i++) { 
        (function(j) {  // j参数也是属于函数隐式声明的变量
            setTimeout(function timer() { 
                console.log(j); 
            }, j*1000); 
        })( i ); 
    }
    

    等等,解决这个问题的方法是每次迭代我们都需要一个块作用域,那么用let来生成块作用域不就搞定了吗?

    for(let i=1; i<=5; i++) {  // 使用let声明i
        setTimeout(function timer() {
            console.log(i);
        }, i*1000);
    }
    

    但let的作用不仅仅是生成块作用域,for循环头部的let声明还会有一个特殊的行为:变量i在循环过程中不止被声明一次,每次迭代都会声明,随后的每个迭代都会使用上一个迭代结束时的值来初始化这个变量。

    这种每次迭代重新声明绑定的行为就类似这样:

    for (var i=1; i<=5; i++) { 
        let j = i;  //每个迭代重新声明j并将i的值绑定在这个块作用域内
        setTimeout( function timer() { 
            console.log(j); 
        }, j*1000); 
    }
    

    这样一路看下来,感觉闭包好像也不是那么神秘嘛,我个人理解的话会把以上归纳为:只要发生了函数传递与调用,就会产生闭包。好了,了解了闭包是什么,那下面来看看它有什么用途。

    二、闭包的应用

    2.1 模块

    闭包最大的作用莫过于创建模块了:

    function betterModule() {
        var name = 'BetterMan';
        var arr = [1, 2, 3];
        function getName() {
            console.log(name);
        }
        function joinArr() {
            console.log(arr.join('-'));
        }
        return {
            getName: getName,
            joinArr: joinArr
        }
    }
    
    var foo = betterModule();
    foo.getName();  // 'BetterMan'
    foo.joinArr();  // '1-2-3'
    

    以上就是一个利用闭包来创建的模块,我们来理一理这段代码:

    首先,betterModule()只是一个函数,必须要通过调用它来创建一个模块实例。如果不执行外部函数,内部作用域和闭包都无法被创建。

    其次,betterModule()返回一个用对象字面量语法{key: value, ...}来表示的对象,这个返回的对象中含有对内部函数而不是内部数据变量的引用,保持了内部数据变量是隐藏且私有的状态,可以将这个对象类型的返回值看作本质上是模块的公共API。

    这个对象类型的返回值最终被赋值给外部的变量foo,然后就可以通过它来访问API中的属性,如foo.joinArr()

    tip: 从模块中返回一个实际的对象并不是必须的,也可以直接返回一个内部函数。jQuery就是如此,jQuery$标识符就是jQuery模块的公共API,但它们本身都是函数(由于函数也是对象,它们本身也可以拥有属性)。

    以上的betterModule函数可以被调用任意多次,每次调用都会创建一个新的模块实例;但如果我们只需要一个实例时,可以对这个模式进行简单的改进来实现单例模式

    var foo = (function betterModule() {
        var name = 'BetterMan';
        var arr = [1, 2, 3];
        function getName() {
            console.log(name);
        }
        function joinArr() {
            console.log(arr.join('-'));
        }
        return {
            getName: getName,
            joinArr: joinArr
        }
    })();
    

    我们将模块函数转换成了IIFE,立即调用这个函数并将返回值直接赋值给单例的模块实例foo。

    2.2 柯里化

    柯里化也用到了闭包,听起来有点高大上,那什么是柯里化呢?

    柯里化(Currying)是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数且返回结果的新函数的技术,看起来是不是有点绕,下面看看例子:

    function add(a, b, c) {
        return a + b + c;
    }
    console.log(add(1,2,3));  // 6
    
    function newAdd(a) {
        return function(b) {
            return function(c) {
                return a + b + c;
            }
        }
    }
    console.log(newAdd(1)(2)(3));  // 6
    

    看着例子对照着定义,看起来描述得还是挺贴切的嘛,其实上面也是利用了闭包的功能绑定了参数的作用域,使得每次调用函数时可以访问上次所传入的参数。

    三、闭包的注意事项

    通常,函数的作用域及其所有变量都会在函数执行结束后被销毁。但是,在创建了一个闭包以后,这个函数的作用域就会一直保存到闭包不存在为止,因为闭包就是一个函数引用另外一个函数的变量,因为变量被引用着所以不会被回收。这是优点也是缺点,不必要的闭包只会徒增内存消耗,所以我们在使用的时候需要注意这方面。

    function add(x) {
      return function(y) {
        return x + y;
      };
    }
    
    var add3 = add(3);
    var add5 = add(5);
    
    console.log(add3(2));  // 5
    console.log(add5(5));  // 10
    
    // 需要手动释放对闭包的引用
    add3 = null;
    add5 = null;
    

    以上的add3add5都是闭包,它们共享相同的函数定义,但是保存了不同的环境。在add3的环境中,x为3。而在add5中,x则为5,最后我们通过null手动释放了add3add5对闭包的引用。

    最后

    如果到了这里你恍然大悟:原来在我的代码中已经到处都是闭包了,只是平时没注意到而已!那说明我这药方还是有点效果的,如果真的如此,那就来波点赞关注吧,因为你的支持就是我最大的动力!

    GitHub传送门
    博客园传送门

  • 相关阅读:
    datagrid表格宽度设置成100%,宽度却不是100%,反而很窄
    Spring MVC 和Struts2对比
    在不知道用户名密码的情况下登录oracle数据库
    IE浏览器的兼容性问题
    在spring mvc 中读取配置文件(porperties)的信息
    godaddy设置方法 控制面板
    easyui datagrid 动态改变大小
    15 个最佳 jQuery 翻书效果插件
    windows7 如何关闭开机启动讲述人
    存储过程代码审核
  • 原文地址:https://www.cnblogs.com/BetterMan-/p/10180779.html
Copyright © 2011-2022 走看看