闭包(已完结)

闭包

通过前面‘词法作用域’、‘LHS与RHS查询’、‘函数声明与变量声明的提升’这三个文案的铺垫,现在我们来谈谈闭包。在此,并不好通过语言去描述闭包,只能通过例子去体会。

在讲解闭包前,你需要先知道以下知识:

一个函数在执行完毕后,这个函数所创建的词法作用域通常会被销毁(用‘通常’来修饰是因为闭包会保存这个词法作用域,所以从闭包的角度来说,并没有销毁,而且这个作用域还会在需要的时候被使用)

闭包的实质就是——函数在本身定义时的词法作用域之外执行,但是函数仍保存了在它定义时的词法作用域。闭包使得函数可以继续访问定义时的词法作用域
无论通过何种手段将内部函数传递到所在的词法作用域以外,它都会持有对原始定义作用域的引用,无论在何处执行这个函数都会使用闭包(也可以通过这句话去理解闭包的实质)

闭包在函数定义时就已经形成,只是我们使用与否

什么是闭包

看下面代码:

    function foo() {
        var a = 2;
        function bar() {
            console.log( a ); // 2
        }
        bar();
    }
    foo();

这是闭包吗?——当然不是!函数bar在其本身定义时的词法作用域(即foo函数的词法作用域)之内执行,这只是普通的词法作用域中的LHS与RHS查询

但是上述代码已经形成了闭包(因为最开始就说了——闭包在函数定义时就已经形成,一旦函数进行了传递等操作使函数在定义时的作用域外执行,那么,那时闭包才会发生作用),只是这儿的整个执行过程并没有使用到闭包而已!(这儿的闭包就是——bar函数已经保存了它定义时的词法作用域——即foo的作用域以及全局作用域,一定不要忘记全局作用域)

再看下面代码,它清晰地展示了闭包效果:

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

bar在在自己定义时的词法作用域以外的地方执行

在foo() 执行后,通常会期待foo()的词法作用域都被销毁,因为我们知道引擎有垃圾回收器用来释放不再使用的内存空间。由于看上去foo() 的内容不会再被使用,所以很自然地会考虑对其进行回收。

但是由于闭包的存在,事实上foo的词法作用域依然存在,因此没有被回收。谁在使用foo的词法作用域?原来是bar()本身在使用(不要忘记,bar本身也保存了全局作用域,当然,‘bar保存了全局作用域’这个说法或许并不正确!但是,实际是,只要foo的词法作用域还查询不到想要的标识符的话,LHS或RHS总会到全局作用域中去找,所以如果你认为‘bar保存了全局作用域’这个说法存在问题,那么从这个角度去理解也是合乎逻辑的)

拜bar() 所声明的位置所赐,它拥有涵盖foo() 内部作用域的闭包,使得该作用域能够一直存活,以供bar() 在之后任何时间进行引用。

bar() 依然持有对该作用域的引用,而这个引用就叫作闭包

因此,在几微秒之后变量baz被实际调用(调用内部函数bar),不出意料它可以访问定义时的词法作用域,因此它也可以如预期般访问变量a

2个小练习

    function foo() {
        var a = 2;
        function baz() {
            console.log( a ); // 2
        }
        bar( baz );
    }
    function bar(fn) {
        fn(); //这就是闭包!
    }
    var fn;
    function foo() {
        var a = 2;
        function baz() {
            console.log( a );
        }
        fn = baz; // 将baz 分配给全局变量
    }
    function bar() {
        fn(); //这就是闭包!
    }
    foo();
    bar(); // 2

上述的闭包代码已经十分直观了!因为为说明该问题,它们的结构都进行了人为的修饰

隐藏的闭包

‘隐藏的闭包’就是说,平时在代码中不那么明显的闭包,虽然它确实是闭包,但是无论从结构或形式上都不像上一节的代码那么明显

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

没错!上述代码是闭包的!——timer保存了wait的词法作用域(乐意的话!你还可以认为它保存了全局作用域)

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

wait(..) 执行1000 毫秒后,它的内部作用域并不会消失,timer 函数依然保有wait(..)作用域的闭包

深入到引擎的内部原理中,内置的工具函数setTimeout(..) 持有对一个参数的引用,这个参数也许叫作fn 或者func,或者其他类似的名字。引擎会调用这个函数,在例子中就是内部的timer 函数,而词法作用域在这个过程中保持完整

除此之外,闭包还潜藏在定时器(前面)、事件监听器(回调函数)、Ajax请求、循环、…中。说的绝对一点,在使用了回调函数的地方都存在闭包!

闭包共享作用域

首先看一个较简单明显的例子:

    function test(){
        var a = 100;
        function foo(){
            a = 200;
        }
        function bar(){
            console.log(a);
        }
        return [foo, bar];
    }
    var funArr = test();
    funArr[0]();
    funArr[1]();//200

显然,因为闭包,foo与bar都保存了test的作用域,而foo对a的修改直接影响了bar的执行结果,因此显而易见,foo与bar共享同一个test作用域(这也是闭包的一个特点)

再看一个例子:

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

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

这样说的话,当然所有函数共享一个i 的引用。循环结构让我们误以为背后还有更复杂的机制在起作用,但实际上没有

如果我就要输出1~5,怎么办?——使用闭包

这样吗?

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

bu!!!应该这样

    for (var i=1; i<=5; i++) {
        (function(j) {//也可以将形参定义为i
            setTimeout( function timer() {
                console.log( j );
            }, j*1000 );
        })( i );
    }

在迭代内使用IIFE 会为每个迭代都生成一个新的作用域,使得延迟函数的回调可以将新的作用域封闭在每个迭代内部,每个迭代中都会含有一个具有正确值的变量供我们访问
函数每执行一次就会生成一个新的作用域

ps:本文参考并引用下列书籍
《你不知道的JavaScript》(上卷)

你可能感兴趣的:(JS)