美文网首页
如何更容易理解js中的闭包

如何更容易理解js中的闭包

作者: coolcao | 来源:发表于2019-10-16 21:37 被阅读0次

    闭包是js中一个晦涩难懂的一个概念,网上关于闭包的文章也是抓一大把,每个人的文章却又不尽相同,或者说,每个人的理解都不一样。

    什么是闭包

    阮一峰老师的一篇文章中说:闭包就是能够读取其他函数内部变量的函数。可以把闭包简单理解成"定义在一个函数内部的函数"。
    在本质上,闭包就是将函数内部和函数外部连接起来的一座桥梁。

    这个解释不能说错,但觉得有点片面。阮老师是从函数内部变量的角度去看闭包。
    也有一个国外的哥们的一篇文章,是这么说的: 闭包是由函数引用其周边状态(词法环境)绑在一起形成的(封装)组合结构。

    我们来看一下MDN上对于闭包的解释:闭包是指那些能够访问独立(自由)变量的函数 (变量在本地使用,但定义在一个封闭的作用域中)。换句话说,这些函数可以“记忆”它被创建时候的环境。

    好吧,这个解释更难懂。。。。

    在阮老师的解读中,一个点就是,在函数外部读取函数内部的变量,阮老师所说的闭包就是将函数内部和外部链接起来的一座桥梁也有失偏颇。

    在《你不知道的js》这本书的《闭包作用域》这一章节中,有这样的描述:

    “当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行。”
    摘录来自: Kyle Simpson、赵望野、梁杰. “你不知道的JavaScript(上卷)”。 iBooks.

    这个描述中,有两个关键的点:一个是,函数,一个是记住并访问所在的词法作用域。这也和MDN上对于闭包的解释相吻合。我们来看一下MDN上的关于闭包的其他描述:

    闭包是一种特殊的对象。它由两部分构成:函数,以及创建该函数的环境。环境由闭包创建时在作用域中的任何局部变量组成。

    所以,到这里,我们可以看出,要理解闭包,我们要抓住两个点:一个是函数,另一个就是创建该函数的环境,所在的词法作用域。

    闭包的案例说明

    关于闭包的概念上的描述,就差不多如上面,可能光看上面的描述还是不能理解什么是闭包。
    那就找两个例子来说明一下。

    来一个最简单的例子,也是大家举的最多的例子:

    var fn = function () {
        var a = 'a in fn';
        var b = function () {
            return a;
        }
        return b;
    }
    
    var f = fn();
    console.log(f());
    

    这个例子估计是大家用来解释闭包用到的最多的例子。在函数fn()内部,定义了另外一个函数b(),函数b()能够访问fn()内部的变量a,是因为b在fn()的词法作用域内。然后我们将b()作为返回值返回,并赋值给变量f,实质上f和b两个都是指向了同一个函数,只是标识符不同而已。因为这个函数能够访问fn()内部的词法作用域,能访问fn()内部的变量a,因此,f()执行的时候就能访问fn()内部的变量a,这就是闭包。

    好了,我们用上面说的两个关键点来慢慢分析闭包:
    首先,一个关键点函数,函数是哪个?b还是f?都是,因为这两个实质是两个标识符指向了同一个函数。第二个关键点,记住函数所在的词法作用域。作用域是哪个,就是函数fn()的词法作用域。当函数fn()执行完毕后,f()还能继续访问fn()内的变量a,这里b()或者f()就是闭包。

    到这里,可能有人就开始喷了,你妹的,这不就是阮老师说的闭包是能够读取其他函数内部变量的函数嘛!!!是的,阮老师的说法并没有错,只是有点片面,为何?

    我们来看下个例子:

    function wait(message) {
    
        setTimeout( function timer() {
            console.log( message );
        }, 1000 );
    
    }
    wait( "Hello, closure!" );
    

    在函数wait()内部,将一个函数timer()传递给定时器。这里timer具有涵盖wait(..)作用域的闭包,因此还保有对变量message的引用。

    当wait()执行1000毫秒后,它的内部作用域并没消失,timer()函数依然保持有对wait()函数作用域的闭包。

    比如,再来一个例子,就是在for循环中的闭包。这个例子被经常用作考察es6的let和var的区别,说实话已经用烂了。。。

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

    很明显,上面代码每隔1秒输出一个5。可能在面试的时候,面试官会要求写出一个代码,从1开始,每隔1秒输出2,3,一直到5。或者怎样,哈哈,反正就是类似的吧。

    大家对于上面这个代码估计也已经烂熟了,肯定不会这么写。可是,大家有没有想过为什么结果是这样呢?有的人可能会说 ,很明显嘛,我们设置了5个定时器,但这5个定时器里函数是异步执行的,当for循环结束时,i是5,所以输出的都是5。

    这说法吧,对,但没说到根上,因为呢,es6添加了个let就不这样:

    for(let i=0;i<5;i++){
        setTimeout(function () {
            console.log(i);
        },i*1000);
    }
    

    这个代码,输出的就是每隔1秒输出一个i,而不是5个5。

    为什么?我们就说说这个问题的根源。

    先说var这个。

    我们传递给setTimeout()的函数,形成一个闭包,我们总共设置了5个setTimeout()共形成5个闭包,这5个闭包共享一个全局的词法作用域,因此共享一个i。当循环结束后,实际上传递给这5个定时器的i是同一个,都是5。

    而对于let,由于let的块级作用域,for循环头部的let不仅将i绑定到了for循环的块中,事实上它将其重新绑定到了循环的每一个迭代中,确保使用上一个循环迭代结束时的值重新进行赋值。也就是说,对于这5个闭包而言,每次i都是重新赋值,因此不会存在var上面的问题。

    关于let和var的具体区别,请参考:深入浅出ES6(十四):let和const

    对,这里的根就和闭包有关系。

    在let之前,可能有的人会提出如下的方案:

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

    这里我们就是用的闭包的作用解决的var的问题。我们用自执行函数IIFE为每个循环生成一个新的作用域,每个循环的setTimeout()内的函数的作用域封闭在每个循环内部。也就是,我们为每个循环生成了一个新的块级作用域,这样使得每次循环都能取得正确的值。let就是这个原理,只不过是简化了代码而已。

    可能有的人还在迷糊,这里有两层函数,哪个是闭包呢?

    为方便分析,我们给函数加上标识符:

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

    每次循环的自执行函数,我们命名为iife(),定时器中的函数我们命名为timer()。这里的闭包是timer()函数形成了对iife()函数作用域的闭包。

    我们说过,要分析闭包,就要搞清楚两个关键点:函数,和其所在的作用域。
    函数,是timer()函数,其所在的作用域就是iife()函数作用域。当函数iife()函数执行完,定义完成立即执行(自执行),其每个timer()函数在定时器内还是能够访问其作用域内的变量,因此,timer()就是闭包,对每个iiff()函数作用域的闭包。

    本质上无论何时何地,如果将函数(访问它们各自的词法作用域)当作第一级的值类型并到处传递,你就会看到闭包在这些函数中的应用。在定时器、事件监听器、Ajax请求、跨窗口通信、Web Workers或者任何其他的异步(或者同步)任务中,只要使用了回调函数,实际上就是在使用闭包!
    摘录来自: Kyle Simpson、赵望野、梁杰. “你不知道的JavaScript(上卷)”。 iBooks.

    再来一个时间的例子:

    function process(data){
        //做一些有趣的事
    }
    var someReallyBigData = {..};
    process(someReallyBigData);
    var btn = document.getElementById('mybtn')
    btn.addEventListener('click',function click(evt){
        console.log('button clicked');
    })
    

    click点击函数并不需要someReallyBigData变量,当process()执行完后,在内存中占用大量空间的数据结构就可以被垃圾回收了。但是,由于click函数形了一个覆盖整个作用域的闭包,javascript引擎极有可能依然保存着这个结构(这取决于具体实现)。

    上面这个例子是《你不知道的js》中解释块级作用域的一个例子。当然放在这里用说明对于事件监听形成的闭包的一个简单例子。

    小结

    闭包是什么,可以直接用MDN上对于闭包的说明进行回答。但是,如要想要真正理解闭包,请从两个关键点去分析:函数,和其创建时所在的词法作用域。
    函数能够记住其所在的词法作用域,即使在其作用域之外执行,这就形成了闭包。

    作为一个合格的面试官,请不要直接问“什么是闭包”这种问题了,估计没有人都说清楚。要想考察对于闭包的理解,可以模拟几个用闭包解决的场景来考察,比如上面的几个例子。

    相关文章

      网友评论

          本文标题:如何更容易理解js中的闭包

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