1

闭包

一、闭包是什么?

将一个 词法作用域 中的 内部函数 作为一个 一级值类型 到处传递,就形成了闭包。

怎么去理解呢?这里要敲黑板划重点了,上面的概念性文字介绍了三个点:

  • 词法作用域(函数)
  • 内部函数
  • 一级值类型传递
1、先说词法作用域

形成一个作用域最常见的就是函数了,函数内部会形成一个内部作用域,然后还有 let 、const 以及像 try/catch 结构中的 catch 分句形成的块作用域。

let 就是为其声明的变量隐式劫持了所在的块作用域,这个在后面讲 let 和闭包的时候会详细说明 let 和闭包结合的用法。

通过了解可以知道,这里的作用域其实就是函数的内部作用域

2、内部函数

内部函数不用介绍了吧,在词法作用域中定义的函数,传递后具有涵盖自身所在作用域的闭包。

3、一级值类型传递

值类型传递方式有很多种啊,函数里面的一级值传递无非就是:返回值return )、赋值( 赋值给外部变量 )、参数传递( 作为参数传递给外部函数 )。

现在可以画一个基本的闭包出来了:

//三种传递方法①②③分开看,你可以的。

var fn;                //定义全局变量,用于内部赋值        ---②
function foo() {
    var a = 2;
    function bar() {
        console.log(a);
    };
    return bar;        //返回值        ---①
    fn = bar;        //赋值        ---②
    baz(bar);        //参数传递    ---③
};

//定义外部函数,用于使用内部分配给全局变量的函数    ---②
function cat() {
    fn();
};

//定义外部函数,用于内部参数传递        ---③
function baz(func) {
    func();
};

foo();        //2       ---①
cat();        //2        ---②
baz();        //2        ---③

再来一例:

function wait(message) {
    setTimeout(function timer(){
        console.log(message);
    },1000);
}
wait("Hi Baby");

解析一下,按照我们前面的思路可以贯穿下来:

首先 wait(..) 里面的作用域,作用域内部的 timer(..)函数,再将内部函数 timer(..) 传递给内置工具函数 setTimeout(..),setTimeout(...)有参数引用( 也就是我们传递的 timer(...) ),然后调用它。

整个过程行云流水,然后词法作用域在这个过程中保持完璧之身。OK!

二、循环中的闭包

说到循环闭包就要掏出大家耳熟能详的栗子了。

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

666!好!输出了几个6,老铁有点懵逼,不知应该扎心还是双击666。

为何?

你大爷还是你大爷,即使你在每次迭代都定义了函数,但是都在共享全局作用域中,i 还是这个 i

那要怎么解决?

这时候在每个迭代的时候加上一个闭包作用域,并且你得把这个 i 大爷放进作用域中。

//放法可以是传参①,可以是赋值②

for(var i = 1; i <= 5; i++) {
    //这里先搞一个闭包作用域,派出我们的 IIFE
    (function(j) {        // ---①
        setTimeout(function timer() {
            console.log(j);
        },j*1000)
    })(i);        // ---①

    (function() {    
        var j = i;        // ---②
        setTimeout(function timer() {
            console.log(j);
        },j*1000)
    })();    
}

上面这个是用了闭包作用域,每次迭代都生成一个新的作用域,来封闭内部变量。

说到这里,前面提到的 let 应该还有人记得,let 干嘛用的,不就是劫持变量形成块作用域吗? 放在这里不是恰到好处? 来一发。

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

直接在定义 i 大爷的地方就 "绑架" 了他。

或者,你也可以麻烦一点,先让他上迭代车,上车之后再 let 定义一个变量把 i 大爷赋给他,两种都行,简单点好。

三、总结一下闭包应用

定时器、事件监听器、Ajax请求、跨窗口通信、Web Workers 或者其他的异步(或同步)任务中(balabala~~~~),只要使用了回调函数,就是在使用闭包。

还有一处重要的 模块

模块的两个重要特征:

  1. 有外部包装函数(创建内部作用域)且需要被调用。
  2. 外部包装函数返回值至少引用一个内部函数(创建包装函数内部作用域闭包)。

蓝染
15 声望3 粉丝