4

由于前段时间项目没有那么忙,然后我这人一天不看点啥就非常焦虑,于是二刷《你不知道的JavaScript》,现在读到闭包,想着看完这一章节,写点东西也是挺好的,所以有了下面的内容,如有不对的地方,敬请斧正,欢迎探讨。

作用域

我们一般讲到闭包,就会谈到作用域,那么作用域又分为了函数作用域块级作用域 ,在这里,我们简单的介绍一下这两种作用域。

函数作用域

函数作用域是指,属于这个函数的全部变量都可以在整个函数的范围内使用及服用(事实上在嵌套的作用域中也可以使用)。
我们先来看一个例子

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

可以看到,这种技术虽然能解决一些问题,但是也会导致其他的问题。首先,必须声明一个具名函数foo(),意味着foo这个名称本身“污染”了所在的作用域。其次,必须显式地(有隐式和显式的区别,这里暂且不表)通过函数名(foo())调用这个函数才能运行其中的代码。
那么我们有没有其他的办法呢,继续往下看。

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

比较一下这两段代码。第一段中foo被绑定在所在所用域中,可以直接通过foo()来调用调用它。第二段中foo被绑定在函数表达式自身的函数中而不是所在作用域中。

块级作用域

在JavaScript中,并不支持块作用域,但是我们为什么还要说它,因为它的风格在JS开发中很常见。
在日常的开发或者学习工作中,我们其实经常能见到类似块作用域,思考以下代码:

for(var i=0; i<6; i++){
    console.log(i);
}

在for循环的头部直接定义了变量 i,通常是因为只想在for循环内部的上下文中使用 i,而忽略了 i 会被绑定在外部作用域(函数或全局)中的事实。当使用var时,它写在哪里都是一样的,因为它们最终都会属于外部作用域。
块作用域是一个用来对之前的最小授权原则进行扩展的工具,将代码从在函数中隐藏信息扩展为在块中隐藏信息。

总结为:任何声明在某个作用域内的变量,都将附属于这个作用域。

闭包

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

继续思考下面代码:

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

看到了吗,这就是闭包的效果。
函数bar()的词法作用域能够访问foo()的内部作用域。然后将bar()函数本身当作一个值类型进行传递。
foo()执行后,其返回值赋值给变量baz并调用baz(),实际上只是通过不同的标识符引用调用了内部的函数bar()
我们知道,JavaScript引擎有垃圾回收器用来释放不再使用的内存空间。而闭包却能阻止这件事情发生。事实上内部作用域依然存在,而没有被回收。
由于bar()的声明位置使它拥有涵盖foo()内部作用域的闭包,使得该作用域能够一直存在,以供bar()在之后进行引用。
再看下面例子

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

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

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

循环与闭包

先看看最常见的for循环。

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

你觉得最后会输出什么,每秒输出一次,分别输出1~5?

那就错啦,实际上,它是会每秒输出一次,但输出~对,就是66666。
为什么?
延迟函数的回调会在循环结束时才执行,而循环结束的条件就是i不再<=5。当定时器运行时,即使每个迭代中执行的是setTimeout(...,0),所有的回调函数依然是在循环结束后才被执行,所以每次都输出6。
根据作用域的原理,尽管循环中的五个函数是在各个迭代中分别定义的,但是它们都被封闭在一个共享的全局作用域中,因此只有一个i。
知道原因之后,我们可以对代码进行一些改造,看看有没有好事发生。

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

Fine,我们终于改造好了,拥有了更多的词法作用域。在迭代中使用立即执行函数(IIFE)会为每个迭代都生成一个新的作用域,使得延迟函数的回调可以将新的作用域封闭在每个迭代内部,每个迭代中都会有一个具有正确值的变量。

到这里,小菊花课堂之JavaScript闭包的内容就告一段落啦,感谢各位能耐心看到这里。

此时是0点52分,时候也不早了,该洗洗睡啦。
see u ~ again


Charles
18 声望2 粉丝

打杂,撸码,玩球,家庭煮夫