喂,别忙着过七夕了,闭包彻底搞懂了吗?

970 阅读5分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第6天,点击查看活动详情

喂喂喂,就是你。进来的小子,有对象的,我先祝福,没对象的,也别哭,我也一样😎。

思维导图

image.png

闭包无处不在

闭包是基于词法作用域书写代码时所产生的自然结果,比如在函数中嵌套了函数。

闭包 的特点就是哪里都可以调用它。管tm天上地下。

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

闭包的定义

函数嵌套

这次守点规矩,在声明的词法作用域里面调用闭包。

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

不知道RHS查找的看看这篇哦。

从编译的角度来学作用域!

bar()函数现在自身作用域查找变量a,没得那就去外侧foo函数的作用域找。找到了,拿来就白嫖。不说什么闭包概念我们也好像可以理解这种就是词法作用域的嵌套嘛。答对了这就是闭包的子集(在声明词法作用域内调用)。

孩子大了,要乱跑。去外面的时世界看看

这次讲bar函数拿到foo函数作用域外面的全局作用域去调用。

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

这个函数在定义时的词法作用域以外的地方被调用。但是还是“白嫖”到了变量a,闭包使得函数可以继续访问定义时的词法作用域。

做个比喻

闭包就是你,你在家(自身词法作用域)拿钱买东西(调用查找变量),你爸妈敢不给?你要和女朋友去外面过七夕了(其他作用域),要钱你爸妈敢不给?给,给,给啊。

闭包的使用

加个定时器

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

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

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

闭包使用场景

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

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

因为函数(示例代码中 的 IIFE)并不是在它本身的词法作用域以外执行的。它在定义时所在的作用域中执行(而 外部作用域,也就是全局作用域也持有 a)。a 是通过普通的词法作用域查找而非闭包被发现的。

IIFE(立即调用函数)

立即调用函数会创建一个包裹自己的作用域。

var a = 2;
(function IIFE() {
    console.log(a); // 2 RHS查找
})();

立即调用函数是一个“宅男”,永远在自己的作用域里面调用。它创建了闭包,是最常用来创建可以被封闭起来的闭包的工具。

循环和闭包

共享作用域

for (var i = 0; i < 10; i++) {
    setTimeout(function () {
        console.log(i) // 全是10
    }, 0)
}

这是为什么,每次循环我们都声明了一个匿名函数到事件队列,但是这些声明的函数都是共享的一个作用域,等到for循环执行完毕,i = 10,那么这些函数就该执行了,自然都会去找这个i = 10的麻烦。

IIFE破冰

套了个壳

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

每次创建匿名函数都会创建一个作用域,可惜还是6。为什么,生儿子再多,都是败家子,还不得啃老,来白嫖变量i。原来和上面一样了。创建的作用域也不就是多个“壳子”。

传递参数

核心 将每次循环的变量i即时保存进自己的作用域,每次循环每个作用域保存的变量i值就不一样了。

下面这2种都行。

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);
}

使用let

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

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

xdm,模块那就先鸽了,我要去过七夕了,哦,忘了,我还是一个人~🤷‍♀️🤷‍♀️