JS之闭包

1. 先说定义:

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

2. 理解闭包需要知道的知识:作用域

闭包产生的前置条件是作用域。JS的有两种作用域:全局作用域函数作用域,且作用域之间可以相互嵌套。就像下面这样:

微信图片_20220610165142.png

这里有三级作用域:全局作用域 - foo()函数的作用域 - bar()函数的作用域,分别对应 1 - 2 - 3

3. 再放一段经典的产生闭包的代码:

function foo() {
    var a = 2;

    function bar() {
        console.log(a);
    }

    return bar;
}

var baz = foo();

baz(); //2 

4. 一个产生闭包的关键角色:垃圾回收器

正常情况下foo()函数在执行结束以后,foo()整个的作用域都会被垃圾回收器销毁。但是var baz = foo()这里变量bazfoo()函数返回的bar()函数的引用,且bar()中还使用了foo()的变量,因此foo()函数的作用域会一直存在,且可以在foo()函数作用域之外还可以访问foo()的作用域。这样就产生了一个闭包。

5. 闭包的一种使用场景

先看一个比较经典的面试题:

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

接触过这道题的人应该都知道这段代码执行结果是:1s后同时输出5个6。

但是我们期望的结果肯定不是这样的,我们希望的结果是1s后输出1 2 3 4 5

产生这种结果的原因是:

  1. 为什么是6: 使用var关键字是可以声明重复的变量,且for循环的{ .. }内又不属于一个与全局作用域区开来的作用域,因此当这个for循环结束以后,会在全局声明一个名称为i变量,它的值是6。
  2. 为什么都是6:因为setTimeout定时器属于事件循环的消息队列的内容,它并不会在JS主线程中同步执行,所以在for循环结束以后才会执行这5次setTimeout定时器,但是此时console.log(i)中的 i 此时访问的是全局的i变量,也就是固定的6。

那么,如何通过闭包解决这个问题?

有两种常用的解决方案。先放代码:

// 最好解决方法:
for (let i = 1; i <= 5; i++) {
	setTimeout(() => {
		console.log(i);
	}, 1000)
}

// 用IIFE也可以实现
for (var i = 1; i < 5; i++) {
	(function (i) {
	    	setTimeout(() => {
	    		console.log(i);
	    	}, 1000)
	})(i)
}

这是如何解决问题的?

第一种方法:

第一种方法,直接将for循环中的var关键字改成了let关键字。

let关键字声明变量有以下几个特点:

  1. let关键字可以将变量绑定到当前所在的作用域(通常是{ .. }内部)
  2. 不存在变量提升。即:使用let声明的变量的使用一定要在声明之后使用。
  3. 在块级作用域以外的地方无法访问声明的变量。
  4. 不允许使用let关键字多次声明同一个变量。

在方法一中起作用的就是以上let声明变量四个特点中的1和3,

  • 改成let关键字,每次循环的i都被绑定在当前for循环的的块级作用域中,且这个i是无法被for循环以外的作用域访问到的。这样就形成一个封闭的作用域,for循环结束以后也就不会在全局作用域出现一个i变量。
  • 同时由于setTimeout定时器又引用了当前for循环的块级作用域的i,因此在每次循环结束以后,这个for循环的{ .. }的块级作用域并不会马上被垃圾回收器释放,此时就形成了一个闭包。在for循环结束以后就会形成5个闭包,且每个闭包都有一个i变量,每个i变量也都是正确的值。

第二种方法:

使用IIFE(立即执行函数),虽然for循环结束以后,会有一个全局的i = 6,但是执行定时器的时候访问的i却不是这个全局的i。

在每次循环时,将当前的i通过参数的方式传递给IIFE中的匿名函数,每次for循环时,立即执行函数执行结束,但是其中的匿名函数的作用域由于setTimeout定时器还使用着匿名函数的形参,因此没有立即释放,这样也就形成了闭包。在JS主线程执行结束,执行到5个定时器任务时,因此也可以实现相同的功能。

6. 闭包的其他使用场景

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

posted @ 2022-06-20 08:48  俄罗斯方块  阅读(91)  评论(2编辑  收藏  举报