《你不知道的javascript》学习笔记—第一部分第五章:作用域闭包

启示

闭包并不是一个需要学习新的语法或模式才能使用的工具,它也不是一件必须接受像Luke2一样的原力训练才能使用和掌握的武器。
闭包是基于词法作用域书写代码时所产生的自然结果,你甚至不需要为了利用它们而有意识地创建闭包。闭包的创建和使用在你的代码中随处可见。你缺少的是根据你自己的意愿来识别、拥抱和影响闭包的思维环境。
最后你恍然大悟:原来在我的代码中已经到处都是闭包了,现在我终于能理解它们了

实质问题

下面是直接了当的定义,你需要掌握它才能理解和识别闭包:

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

下面用一些代码来解释这个定义。

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

基于词法作用域的查找规则,函数bar()可以访问外部作用域中的变量a.
技术上来讲,也许是闭包。但根据前面的定义,确切地说并不是。
从纯学术的角度说,函数bar()具有一个涵盖foo()作用域的闭包(事实上,涵盖了它能访问的所有作用域,比如全局作用域)。也可以认为bar()被封闭在了foo()的作用域中。为什么呢?原因简单明了,因为bar()嵌套在foo()内部。

通过这种方式定义的闭包并不能直接进行观察,也无法明白在这个代码片段中闭包是如何工
作的。

下面我们来看一段代码,清晰地展示了闭包:

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

这个例子中,将bar()函数当做一个值类型传递,并将其引用的函数本身当作返回值。

在foo()执行后,其返回值(也就是内部的bar()函数)赋值给变量baz并调用baz(),实际上只是通过
不同的标识符引用调用了内部的函数bar()。
bar()显然可以被正常执行。但是在这个例子中,它在自己定义的词法作用域以外的地方执行。
在foo()执行后,通常会期待foo()的整个内部作用域都被销毁,因为我们知道引擎有垃圾回收器用
来释放不再使用的内存空间。由于看上去foo()的内容不会再被使用,所以很自然地会考虑对其进
行回收。
而闭包的“神奇”之处正是可以阻止这件事情的发生。事实上内部作用域依然存在,因此没有被回
收。谁在使用这个内部作用域?原来是bar()本身在使用。
拜bar()所声明的位置所赐,它拥有涵盖foo()内部作用域的闭包,使得该作用域能够一直存活,以
供bar()在之后任何时间进行引用。
bar()依然持有对该作用域的引用,而这个引用就叫作闭包。

这个函数在定义时的词法作用域以外的地方被调用。闭包使得函数可以继续访问定义时的词法作
用域。
当然,无论使用何种方式对函数类型的值进行传递,当函数在别处被调用时都可以观察到闭包

function foo() {
    var a = 2;
    function baz() {
    console.log( a ); // 2
    }
    bar( baz );
}
function bar(fn) {
    fn(); // 妈妈快看呀, 这就是闭包!
}

把内部函数baz传递给bar,当调用这个内部函数时(现在叫作fn),它涵盖的foo()内部作用域的闭包就可以观察到了,因为它能够访问a。
传递函数当然也可以是间接的。

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)传递给setTimeout(..)。timer具有涵盖wait(..)作用域的闭包,因此
还保有对变量message的引用。

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

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

循环和闭包

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

这段代码在运行时会以每秒一次的频率输出五次6。
事实上,当定时器运行时即使每个迭代中执行的是setTimeout(.., 0),所有的回调函数依然是在循环结束后才会被
执行,因此会每次输出一个6出来。

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

第3章介绍过,IIFE会通过声明并立即执行一个函数来创建作用域

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

我们现在显然拥有更多的词法作用域了。的确每个延迟函数都会将IIFE在每次迭代中创建的作用域封闭起来。

仔细看一下,我们的IIFE只是一个什么都没有的空作用域。它需要包含一点实质内容才能为我们所用。它需要有自己的变量,用来在每个迭代中储存i的值:

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

改进代码

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

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

重返块作用域

每次迭代我们都需要一个块作用域。第3章介绍了let声明,可以用来劫持块作用域,并且
在这个块作用域中声明一个变量。

本质上这是将一个块转换成一个可以被关闭的作用域。因此

for (var i=1; i<=5; i++) {
    let j = i; // 是的, 闭包的块作用域!
    setTimeout( function timer() {
        console.log( j );
    }, j*1000 );
}

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

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

小结

闭包就好像从JavaScript中分离出来的一个充满神秘色彩的未开化世界,只有最勇敢的人才能够
到达那里。但实际上它只是一个标准,显然就是关于如何在函数作为值按需传递的词法环境中书
写代码的。
当函数可以记住并访问所在的词法作用域,即使函数是在当前词法作用域之外执行,这时就产生
了闭包。
如果没能认出闭包,也不了解它的工作原理,在使用它的过程中就很容易犯错,比如在循环中。

One More Thing

本文为原创文章,大部分来自于《你不知道的javascript》一书中,转载请注明出处https://xdlmr.github.io


上一篇
《你不知道的javascript》学习笔记-第二部分第一章:关于this 《你不知道的javascript》学习笔记-第二部分第一章:关于this
this关键字是JavaScript中最复杂的机制之一。它是一个很特别的关键字,被自动定义在所有函数的作用域中。但是即使是非常有经验的JavaScript开发者也很难说清它到底指向什么。 为什么要用thisfunction identify
2016-08-17
下一篇
《你不知道的javascript》学习笔记-第一部分第四章:提升 《你不知道的javascript》学习笔记-第一部分第四章:提升
前言函数作用域和块作用域的行为是一样的,可以总结为:任何声明在某个作用域内的变量,都将附属于这个作用域。但是作用域同其中的变量声明出现的位置有某种微妙的联系,而这个细节正是我们将要讨论的内容。 先有鸡还是先有蛋直觉上会认为JavaScrip
2016-08-13
目录