JavaScript基石——闭包

开篇

作为前端开发人员,闭包一定是大家面试绕不开的知识点,在我们实际开发的过程中我们也会有意无意接触到闭包。本文来带着大家一步步从闭包的概念梳理到其应用场景。

概念

MDN官方中文文档对于闭包的概念是这样定义的------

闭包 (closure)是一个函数以及其捆绑的周边环境状态(lexical environment词法环境)的引用的组合。换而言之,闭包让开发者可以从内部函数访问外部函数的作用域。在 JavaScript 中,闭包会随着函数的创建而被同时创建。

下面我们来复习一下闭包相关的一些基础概念

作用域

分为全局作用域以及局部作用域(包括块级作用域以及函数作用域),在他们中定义的变量分别称为全局变量以及局部变量。

案例如下:

html 复制代码
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>

<body>
    <script>
        var name = "Marry"  // name 是全局变量
        function init() {
            var name = "Mozilla"; // name 是一个被 init 创建的局部变量
            console.log(name)
        }
        init()
    </script>
</body>

</html>

显然,此时的输出结果是Mozilla,因为两者都存在时自己的局部变量的优先级是比全局变量高的

自由变量

自由变量可以理解为不在自己作用域中的变量,示例如下:

html 复制代码
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>

<body>
    <div>nihao</div>
</body>

<script>
    var name = "Marry"  
    function init() {
        console.log(name);  //此时的name是自由变量
    }
    init()
</script>

</html>

那么此时name的值是函数的定义时外层作用域的变量值,name的值与调用的地方是没有关系的。这一点在闭包中是十分重要的概念。

触发场景

知道了闭包的概念以及相关的基础知识以后,让我们来看看哪些情况下会产生闭包。

函数当作返回值被返回

直接上代码,创建返回的函数时,name是其的自由变量,于是我们在他的外层作用域去找对应的name变量,找到了"Ccat"。

虽然我在后面全局作用域中也定义了一个变量name,此时返回的函数已经形成了他的闭包,不会受到外界name变量的影响了,最终输出"Ccat"

html 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>

<script>
    function getName(){
        const name = "Ccat"
        return function(){
            console.log(name);
        }
    }
    const name = "Mouse" 
    const fun = getName()
    fun()
</script>
</html>

函数当作返回值被返回

这是第二种触发场景,按照之前的分析方法分析一遍就可以得到正确的输出结果了。

html 复制代码
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>

<body>

</body>

<script>

    function fn(cb) {
        const name = "mouse"
        cb()
    }

    const name = "Ccat"
    fn(
        function () {
            console.log(name);
        }
    )

</script>

</html>

自定义匿名函数

第三种是自定义匿名函数,后面的案例会有应用。

html 复制代码
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>

<body>

</body>

<script>

    (function(index){
        console.log(index);
    })(100)

</script>.

</html>

应用场景

解决var for循环

大家在学习js基础部分的时候可能会遇到这么一个问题,我通过for循环来为我的元素添加点击事件,点击后输出其对应的index,但是事情并非所愿,如果按照下方的代码去写的话无论我点击哪一个按钮输出的都是3。

html 复制代码
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>

<body>
    <button>0</button>
    <button>1</button>
    <button>2</button>
</body>

<script>

    const buttonItems = document.getElementsByTagName("button")
    for (var index = 0; index < buttonItems.length; index++) {
        buttonItems[index].onclick = function(){
            console.log(index)
        }
    }

</script>

</html>

这种情况的发生是因为我们的点击事件回调函数没有形成自己的闭包,导致当用户点击时,index的值始终为for循环执行后的值,也就是3。

定位到了问题所在,我们的方向就是让回调函数形成自己的闭包,这里使用的是自定义匿名函数的方式,代码如下:

html 复制代码
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>

<body>
    <button>0</button>
    <button>1</button>
    <button>2</button>
</body>

<script>

    const buttonItems = document.getElementsByTagName("button")
    for (var index = 0; index < buttonItems.length; index++) {
        (function (index) {
            buttonItems[index].onclick = function () {
                console.log(index)
            }
        })(index)
    }

</script>

</html>

问题解决!当然我们也可以采用现在开发中最常用的方法,把var改成let,形成块级作用域,这里就不再过多赘述了。

隐藏变量

所谓闭包,就是能够将信息封闭起来。第二个应用场景我们来封装一个具有set、get功能的函数。代码如下:

html 复制代码
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>

<script>

    function newData() {
        const data = {}

        return {
            get: function (key) {
                return data.key
            },
            set: function (key, val) {
                data.key = val
            }
        }
    }

    const dataSet = newData()
    dataSet.set("name","哈哈哈")
    const name = dataSet.get("name")
    console.log(name);

</script>

</html>

当我们在外部想访问函数内部的data时是访问不到的,因为函数形成了闭包。

节流与防抖

这个在业务中就很常见了

节流就是n秒内只运行一次,若在n秒内重复触发,只有一次生效,防抖则是n秒后在执行该事件,若在 n 秒内被重复触发,则重新计时。

节流实现如下(时间戳+定时器结合):

js 复制代码
function throttled(fn, delay) {
    let timer = null
    let starttime = Date.now()
    return function () {
        let curTime = Date.now() // 当前时间
        let remaining = delay - (curTime - starttime)  // 从上一次到现在,还剩下多少多余时间
        let context = this
        let args = arguments
        clearTimeout(timer)
        if (remaining <= 0) {
            fn.apply(context, args)
            starttime = Date.now()
        } else {
            timer = setTimeout(fn, remaining);
        }
    }
}

防抖实现如下:

js 复制代码
function debounce(func, wait, immediate) {

    let timeout;

    return function () {
        let context = this;
        let args = arguments;

        if (timeout) clearTimeout(timeout)
        if (immediate) {
            let callNow = !timeout; // 第一次会立即执行,以后只有事件执行后才会再次触发
            timeout = setTimeout(function () {
                timeout = null;
            }, wait)
            if (callNow) {
                func.apply(context, args)
            }
        }
        else {
            timeout = setTimeout(function () {
                func.apply(context, args)
            }, wait);
        }
    }
}
相关推荐
小政爱学习!13 分钟前
封装axios、环境变量、api解耦、解决跨域、全局组件注入
开发语言·前端·javascript
魏大帅。19 分钟前
Axios 的 responseType 属性详解及 Blob 与 ArrayBuffer 解析
前端·javascript·ajax
花花鱼25 分钟前
vue3 基于element-plus进行的一个可拖动改变导航与内容区域大小的简单方法
前端·javascript·elementui
k093328 分钟前
sourceTree回滚版本到某次提交
开发语言·前端·javascript
时差9531 小时前
【面试题】Hive 查询:如何查找用户连续三天登录的记录
大数据·数据库·hive·sql·面试·database
web行路人1 小时前
React中类组件和函数组件的理解和区别
前端·javascript·react.js·前端框架
番茄小酱0011 小时前
Expo|ReactNative 中实现扫描二维码功能
javascript·react native·react.js
子非鱼9211 小时前
【Ajax】跨域
javascript·ajax·cors·jsonp
超雄代码狂1 小时前
ajax关于axios库的运用小案例
前端·javascript·ajax
周亚鑫2 小时前
vue3 pdf base64转成文件流打开
前端·javascript·pdf