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

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

一、有趣的作用域链

在开始聊闭包之前呢,先和大家来聊一聊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 块级作用域(立即执行函数)

这个例子很经典,说到会计作用域就会想到var和let,在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中的闭包有一个新的认识,能够在我们的工作中去灵活运用。也希望能够在面试中游刃有余,我是勇宝,一个前端小学生。

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

相关推荐
考研保研资料分享21 分钟前
电气工程保研经验:夏令营失利后上岸上交,科研追问与九推复习(2024)
面试
树下水月34 分钟前
Typora破解
linux·服务器·前端
用户5508492902561 小时前
CSS布局实战:从Flex到Grid的完整避坑指南
前端
用户5508492902561 小时前
前端接口请求层怎么封装?一个可落地的请求方案
前端
时间的拾荒人1 小时前
Qt 窗口系统详解:从菜单栏到对话框的完整指南
开发语言·qt·面试
MetWeave1 小时前
跟着一条 SPECI 走流水线:从电报到界面
前端
樱花落木兰1 小时前
分布式登录实战:Session 会话共享改造,Redis 存储用户登录状态
java·javascript·数据库·redis·分布式·缓存
Helix2502 小时前
Chrome 开发者工具进阶:长截图、网络调试与性能分析
前端·chrome·chrome devtools·使用技巧·性能分析·开发者工具·长截图
无忧.芙桃2 小时前
位图妙用:512MB 实现 40 亿整数的存在性查询
c语言·c++·面试·哈希算法
liangshanbo12152 小时前
面试题:线上 JavaScript 报错如何快速定位到源码?
开发语言·javascript·ecmascript