作用域闭包

在讲解作用域闭包的内容之前,需要对以下概念有所掌握:

  1. JavaScript具有两种作用域:全局作用域和函数作用域,至于块作用域也不能说没有,比如说: try ...catch...语句中,catch分句就是块作用域,还有with语句等。

  2. ES6中的let关键字,可以用来在任意代码块中声明变量。

  3. 什么事立即执行函数表达式以及它的作用。

老生常谈什么是闭包

闭包的概念:函数可以记住并访问所在的词法作用域时,即使函数是在当前词法作用域之外执行,这时就产生了闭包。

    function foo(){
        var a = 2;
        function bar(){
            console.log(a);
        }
        return bar;
    }
    var baz = foo();
    baz(); //这就是闭包的效果

函数bar()的词法作用域能够访问foo()的内部作用域,然后我们将bar()函数本身当作一个值进行传递。在foo()执行后,其返回值赋值给变量baz并调用baz()。
在foo()执行后,通常会期待foo()的整个内部作用于都被销毁,因为我们知道引擎有垃圾回收机制来释放不在使用的内存空间。由于看上去foo()的内容不会再被使用,所以很自然地会考虑对其进行回收。
但是,闭包的神奇之处在于可以阻止这件事情发生。事实上内部作用域依然存在,因此,没有被回收。那么是谁在使用这个内部作用域呢?当然是bar()在使用。
由于bar()声明在foo()函数内部,所以它拥有涵盖foo()内部作用域的闭包,使得该作用域能够一直存活,以便bar()在以后的任何时间进行引用。
bar()函数在foo()调用完成后,依旧持有对其作用域的引用,而这个引用就叫做闭包

当然,无论使用何种方式对函数类型的值进行传递,当函数在别处调用时都可以观察到闭包

    function foo(){
        var a = 2;
        function baz(){
            console.log(a)//2
        }
        bar(baz);
    }
    function bar(fn){
        fn(); //这就是闭包
    }

相比于上面代码的枯燥,这有一个更加常见的例子

    function wait(message){
        setTimeout(function time(){
            console.log(message);
        }, 1000);
    }
    wait("hello clousre");

简单分析一下这段代码:我们将一个名为time的内部函数传递给setTimeout(),time具有涵盖wait()作用域的闭包,因此,还保有对变量message的引用。
wait(..)执行1000ms后,它的内部作用域并不会消失,time()函数依旧保有对wait()作用域的闭包,在引擎内部,内置的工具函数setTimeout()会持有一个对参数的引用,这个参数也许叫作fn或者func之类的。引擎会调用这个函数,而词法作用域在这个过程中保持完整。
这就是闭包

那么闭包有哪些应用呢?其实包括定时器,事件监听器,Ajax请求,跨窗口通信,Web Workers或者任何其他的异步(或者同步)任务中,只要使用回掉函数,实际上就是在使用闭包!

这里我们再看一个特别典型闭包的例子,但严格来说它并不是闭包

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

IIFE即立即执行函数表达式,第一个()让函数变为函数表达式,第二个()函数执行。为什么说他严格上来讲并不是闭包呢?因为在示例代码中函数并不是在它本身的词法作用域之外执行的它在其定义时所在的作用域执行,a是通过词法作用域查找到的,并不是闭包发现的。
尽管IIFE本身并不是观察闭包的恰当例子,但他的确创建了一个封闭的作用域,并且也是最常用来创建被封闭起来的闭包的工具。

循环和闭包

说到闭包我们接触最早的也许就是for循环的例子:

    for(var i = 1; i<6; i++){
        setTImeout(function time(){
            console.log(i)
        }, i*1000)
    }

记得第一次看见这段代码的时候,那是被深深的虐到,作为C语言起手的同学,当时真的是一脸的懵逼,为什么会输出5个6, 为什么会输出5个6,为什么?当时其他人的讲解也是模模糊糊的,虽然提出了解决方法,当还是无法理解这其中的机制原理,所以,我痛下决心把它弄懂!也许只有我不懂吧!

问:为什么会输出66666呢?
答:能输出66666说明for循环内部的代码的确执行了5次。
问:那6是从哪来的呢?
答:6是我们循环的终止条件,所以输出6。
问:那为什么不是循环一次,输出一个值, 1,2,3,4,5这样呢?
答:setTimeout()函数是在循环结束时执行的,就算是你设置setTimeout(fn, 0),它也是在for循环完成后立即执行,总之就是在for循环执行完成后才执行。

好了,这就不难理解了为什么会输出66666了。但这也就引出了一个更深入的话题,代码中到底什么缺陷导致它的行为同语义暗示的不一致呢?

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

而我们想象中的样子确是这样。

下面回到正题。既然明白了缺陷是什么,那么要怎样做才能达到我们想象中的样子呢?答案是我们需要在每一次迭代的过程中都创建一个闭包作用域。在上文中我们已经有所铺垫,IIFE会通过声明立即执行一个函数来创建作用域。so我们可以将代码改成下面的样子:

    for(var i=1; i<6; i++){
        (function(){
            setTImeout(function time(){
                console.log(i)
            }, i*1000)
        })();
    }

这样每一次迭代我们都创建了一个封闭的作用域(你可以想象为上图中黄色的矩形部分)。但是这样做仍旧不行,为什么呢?因为虽然每个延迟函数都会将IIFE在每次迭代中创建的作用域封闭起来,但我们封闭的作用域是空的,所以必须传点东西过去才能实现我们想要的结果。

    for(var i=1; i<6; i++){
        (function(){
            var j = i
            setTImeout(function time(){
                console.log(j)
            }, j*1000)
        })();
    }

ok!试试现在他能正常工作吗?对这段代码再进行一点改进

    for(var i=1; i<6; i++){
        (function(j){
            setTImeout(function time(){
                console.log(j)
            }, j*1000)
        })(i);
    }

总的来说,就是在迭代内使用IIFE会为每个迭代都生成一个新的作用域,使得延迟函数可以将新的作用域封闭在每个迭代内部,我们同时在迭代的过程中将每次迭代的i值作为参数传入进新的作用域,这样在迭代中创建的封闭作用域就都会含有一个具有正确值的变量供我们访问。ok,it's work!

块作用域

仔细思考我们前面的解决方案。我们使用IIFE在每次迭代时都创建一个新的作用域。也就是说,每次迭代我们都需要一个块作用域。前面我们提到,你需要对ES6中的let关键字进行了解,它可以用来劫持块作用域,并且在这个块作用域中声明一个变量。
本质上来讲它是将一个块转换成可以被关闭的作用域。

    for(var i=1; i<6; i++){
            let j = i; //闭包的块作用域
            setTImeout(function time(){
                console.log(j)
            }, j*1000)
    }

如果将let声明在for循环的头部那么将会有一些特殊的行为,有多特殊呢?它会指出变量在循环过程中不止被声明一次,每次迭代都会声明。随后的每个迭代都会使用上一个迭代结束时的值来初始化这个变量。不管这句话有多拗口,看看代码吧!

        for(let i=1; i<6; i++){
            setTImeout(function time(){
                console.log(i)
            }, i*1000)
    }

有没有似曾相识的感觉,有没有感动到,我已经老泪纵横了。。。

下一节讲闭包运用--模块机制

你可能感兴趣的:(javascript)