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

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

一、有趣的作用域链

在开始聊闭包之前呢,先和大家来聊一聊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中的闭包有一个新的认识,能够在我们的工作中去灵活运用。也希望能够在面试中游刃有余,我是勇宝,一个前端小学生。

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

相关推荐
counterxing26 分钟前
我整理了一个免费开发资源目录,还做成了 CLI 和 MCP
前端·agent·ai编程
子兮曰7 小时前
Bun v1.3.14 深度解析:Image API、HTTP/3、全局虚拟存储与五十项变革
前端·后端·bun
kyriewen8 小时前
今天,百年巨头一次砍了9200人,而一个离职科学家的实话让全网睡不着觉
前端·openai·ai编程
Cosolar8 小时前
大模型应用开发面试 • 每日三题|Day 003|多Agent系统中的通信协议、冲突解决和一致性保障
人工智能·后端·面试
问心无愧05138 小时前
ctf show web 入门42
android·前端·android studio
kyriewen9 小时前
老板逼我上AI,我偷偷在浏览器里跑LLaMA,省下20万API费
前端·react.js·llm
Beginner x_u9 小时前
前端八股整理(手写 02)|数组转树、数组扁平化、随机打乱一个数组
前端·数组·数组转树·数组扁平化
KaMeidebaby9 小时前
卡梅德生物技术快报|禽类成纤维细胞 FISH 实验:鸟类性别染色体基因定位技术实现与数据验证
前端·数据库·其他·百度·新浪微博
天若有情6739 小时前
前端高阶性能优化:跳出传统懒加载与预加载,基于用户行为做轻量预判加载
前端·性能优化
小小小小宇9 小时前
前端转后端:SQL 是什么
前端