• 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或者任何其它的异步任务中,只要使用了回调函数,实际上就是在使用闭包。

  • 相关阅读:
    Android Studio踩的坑之导入别人的Android Studio项目
    获取已发布微信小游戏和小程序源码
    小程序第三方框架对比 ( wepy / mpvue / taro )
    项目中常用的MySQL 优化
    最全反爬虫技术
    MySQL Explain详解
    php接口安全设计
    PHP进程间通信
    PHP进程及进程间通信
    springBoot优雅停服务配置
  • 原文地址:https://www.cnblogs.com/codexlx/p/16392088.html
Copyright © 2020-2023  润新知