茶余饭后聊聊闭包到底是个什么东东?

闭包是我们每一个前端打工人绕不开的技能点,它经常出现在:同事之间吹水,面试官刁难时等一系列'高端局'中。时至今日,要是问你闭包到底是做什么的,你请答出个一二三吗?

一、有趣的作用域链

在开始聊闭包之前呢,先和大家来聊一聊JavaScript中作用域的概念,在ES5中作用域有两种:全局作用域函数级作用域,我们知道,当我们去访问一个变量的时候,JavaScript会从当前作用域一级一级的往上'冒泡',直到我们的window(全局),由此我们也打趣的叫作用域链,我们通过案例更好的去感受一下:

js 复制代码
var a = 1;

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

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

foo(); // 2
zoo(); // 1
console.log(a); // 1

从案例中我们可以看到,每一层函数是一个单独作用域函数内部可以访问函数外部的变量,如果找不到就会再往上找,直到全局作用域(window)

二、堆和栈傻傻分不清

2.1 内存释放

我们再来聊一聊JavaScript中的堆内存栈内存,我们都知道引用数据类型的值存储在堆内存中(比如我们的函数),一般函数执行完成内存就会被释放掉,下面举例说明一下内存释放

  1. 全局下的变量只有当页面关闭的时候内存才会被释放。

    js 复制代码
    var obj = {
        name: 'iyongbao';
    }
    
    console.log(obj.name); // iyongbao
  2. 如果是函数,执行完成会立即被释放掉

    js 复制代码
    function foo () {
        var a = 2;
        console.log(a);
    }
    
    var f = foo(); // 2
    
    console.log(f); // undefined

2.2 内存释放(特殊情况)

正常情况下,函数执行完成会立即被释放,但是也有特殊情况,那就是如果函数执行完后,函数中的私有变量被作用域外的变量引用的时,栈内存就不会去释放栈内存中的基本值。

js 复制代码
function foo () {
    var a = 2;
    
    function zoo () {
        a++;
        console.log(a);
    }
    
    zoo();
    
    return zoo;
}

var f = foo(); // 3
f(); // 4
f(); // 5

由此我们可以看出,foo函数执行完返回给f变量一个函数,a变量没有被释放。

三、走进闭包

3.1 闭包到底是什么

其实2.2就是一个闭包,由此我们可以总结一下闭包的概念:闭包是指能够访问一个函数内部私有变量的函数。

这里注意闭包是一个函数。

3.2 闭包是如何形成的

当函数存在外部作用域的值的引用时就会产生闭包。

js 复制代码
var a = 3;
function foo (c) {
    var b = 5;
    function zoo () {
        console.log(a + b + c);
    }
    
    zoo();
}

foo();

3.3 闭包的作用

闭包可以使我们的变量私有化,內存不会被销毁,并且使我们的变量很好的与外部作用域隔离,防止变量的全局污染,如果你用过JQuery可能会深有体会。

3.4 闭包的实际应用

3.4.1 块级作用域(立即执行函数)

这个例子很经典,说到会计作用域就会想到varlet,在ES6之前,我们做循环会遇到下面这种情况:

js 复制代码
var fooArr = [];

for (var i = 0; i < 5; i ++) {
    fooArr[i] = function () {
        console.log(i);
    }
}

fooArr.forEach(fn => {
    fn();
})

结果我们发现打印了五次5

因此我们就可以借助闭包来解决这个问题。

js 复制代码
var fooArr = [];

for (var i = 0; i < 5; i ++) {
    fooArr[i] = (function (i) {
        return function () {
            console.log(i);
        }
    })(i)
}

fooArr.forEach(fn => {
    fn();
})
// 0 1 2 3 4

此时得到了我们想要的结果啦!

3.4.2 变量私有

形成一个闭包后,我们可以使用闭包中的变量,下面这个案例留给大家去思考看一看会打印出什么东东。

js 复制代码
function foo() {
    var a = 'foo';
    function zoo() {
        console.log(a);
    }
    
    return zoo;
}

function poo(fn) {
    var a = 'poo';
    fn();
}

poo(foo());

3.4.3 防抖和节流

js 复制代码
// 节流
function throttle(fn, timeout) {
    let timer = null
    return function (...arg) {
        if(timer) return
        timer = setTimeout(() => {
            fn.apply(this, arg)
            timer = null
        }, timeout)
    }
}

// 防抖
function debounce(fn, timeout){
    let timer = null
    return function(...arg){
        clearTimeout(timer)
        timer = setTimeout(() => {
            fn.apply(this, arg)
        }, timeout)
    }
}

3.4.4 函数柯里化

每次只输入一部分函数去处理,该函数会返回一个新函数,再将剩余参数输入到返回的新函数中,最终实现某个功能。

js 复制代码
function add (a, b) {
    return a + b;
}
console.log(add(1+2)); // 3

如果写成柯里化是这个样子的:

js 复制代码
function add (a) {
    return function (b) {
        return a + b;
    }
}

let currey = add(1);
console.log(currey(2)); // 3

// 或者也可以这样写
//console.log(add(2)(1)); // 3

3.5 使用闭包要注意什么

首先是使用闭包容易导致内存泄露,因为闭包会包含其他作用域的变量引用,因此闭包会占用更多的内存空间,所以闭包应该谨慎使用。

四、总结

希望通过阅读本文大家能够对JavaScript中的闭包有一个新的认识,能够在我们的工作中去灵活运用。也希望能够在面试中游刃有余,我是勇宝,一个前端小学生。

最近工作比较忙,网站有几天没有进行维护了,清明回来后会积极的去更新,欢迎大家评论区留言。

相关推荐
崔庆才丨静觅8 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60619 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了9 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅9 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅10 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅10 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment10 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅10 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊10 小时前
jwt介绍
前端