我也不懂闭包呀,直到自己写了这篇文章...

36 阅读3分钟

闭包在JavaScript中无处不在,但是对于很多人(我)来说,他们(我)还是很难去理解这些闭包现象,希望通过这篇文章可以帮助大家(自己)理解闭包。

理解闭包

  在《你所不知道的JavaScript上》中很好的解释了闭包的定义,😎 当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行。

  然后我们马上来举一个🌰:

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

var baz = foo(); // 相当于bar函数在foo函数外部执行(bar函数所在的词法作用域外)
baz(); // 还是引用了bar所在词法作用域内的变量,输出2

简单概述一下上面的代码执行过程:

  1. 声明了一个foo函数,声明一个变量baz,赋值为foo()的返回值

  2. 执行foo(), 声明函数bar,变量a,然后返回bar

  3. baz得到返回值,就是bar函数,执行baz()函数

  4. 执行console.log(a), 向上一层作用域内寻找变量a,在foo函数内,bar函数记住了其所在的词法作用域,并给return了出去。在进行baz=foo()之后,正常来说我们希望foo()执行后,其内部的作用域被销毁(javascript引擎的垃圾回收机制, 会释放不再使用的内存空间),因为在执行baz=foo()之后,看上去我们不再需要使用foo内部的内容了,但是事实上foo函数的内部作用域依然存在着,那么是谁在一直使用该作用域导致了其没有被垃圾回收呢,是bar函数,bar函数拥有着涵盖foo()的内部作用域的闭包,使该作用域一直存活着,console.log(a)中查询变量a即为foo的内部作用域中的变量a, 输出为2。

    好的我讲完了,但似懂非懂,找些例子看看。

例子1 最常见的axios接口封装

import request from "@/api"
const exportRecordList = (data) => {
    return request({
        url: `/api/xxxxxxxxx/export`,
        method: 'post',
        data
    })
}
exportRecordList({age:18})

😶 request函数有着对其所在作用域的闭包(request函数中引用了所在作用域中的data变量),执行完exportRecordList()后,因为返回的函数中有着对exportRecordList()内部作用域的引用,其内部作用域不会被销毁,保持完整。

例子2 一个延时输出的函数

const wait = (str) => {
    setTimeout(function logStr() {
        console.log(str)
    }, 1000);
}
wait('hello, world')

🥲 logStr函数内有着涵盖wait()函数作用域的闭包, 和上面类似。

例子3 一个经典的面试题

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

😥 i=1的时候,var声明的变量挂载到全局上,window.i = 1,执行setTimeout,1s后打印i 😥 i=2的时候,var声明的变量挂载到全局上,window.i = 2,执行setTimeout,2s后打印i

...

😥 i=5的时候,var声明的变量挂载到全局上,window.i = 5,执行setTimeout,5s后打印i 😥 i=6的时候,var声明的变量挂载到全局上,window.i = 6, 不满足i<=5跳出循环

上面的循环大概在几微秒内就完成了,然后过了1s开始打印console.log(i),引擎向作用域讯问 i变量是 啥,作用域向上寻找找到了 i = 6,打印出6, 过了2s继续打印console.log(i), 还是6...

到最后输出结果为 6 6 6 6 6

用闭包改造一下

for (var i = 1; i <= 5; i++) {
    (function fn(j) {
        setTimeout(function timer() {
            console.log(j);
        }, j * 1000);
    })(i);
}
  • i=1的时候,fn接收到i的传参,声明变量j = i (1), timer函数内有着对其所在作用域的引用console.log(j),此时fn()的作用域保留, j = 1, 1s后输出1
  • i=2的时候,fn接收到i的传参,声明变量j = i (2), timer函数内有着对其所在作用域的引用console.log(j),此时fn()的作用域保留, j = 2, 2s后输出2

...

  • i=5的时候,fn接收到i的传参,声明变量j = i (5), timer函数内有着对其所在作用域的引用console.log(j),此时fn()的作用域保留, j = 5, 5s后输出
  • i=6的时候,不满足条件,跳出循环,此时window.i = 6。
  • 最后输出1 2 3 4 5

当然啦,也可以直接用let声明

let 允许你声明一个作用域被限制在作用域中的变量、语句或者表达式

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

输出 1 2 3 4 5

例子4 模块化

function CoolModule() {
    var something = "cool";
    var another = [1, 2, 3];
    function doSomething() {
        console.log( something );
    }
    function doAnother() {
        console.log( another.join( " ! " ) );
    }
    return {
        doSomething: doSomething,
        doAnother: doAnother
    };
}

😭 如果我们调用了CoolModule(),doSomething()和doAnother()都将拥有涵盖CoolModule()内部作用域的闭包

小结

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