也许你并没有那么了解闭包(本文从原因、作用、场景深入了解闭包)

前言

前几天听到同学说在面试被问到闭包的场景,他说:"既然闭包有可能造成内存泄漏,那还用它干嘛,不就没事找事做吗",我本着存在即合理给他解释...

你有没有想过,其实你并没有那么了解闭包,在面试中被问到:"讲一下你了解的闭包",你是否还只会说闭包 = 内层函数使用外层函数变量,由于变量无法回收,所以在函数外部也能使用该变量...

接下来,我们一层一层将闭包解剖,看看闭包到底有什么神奇之处

闭包到底是什么?

通常情况下,函数内部变量是无法在外部访问的(即全局变量和局部变量的区别),通过闭包实现了能 在外部访问某个函数内部变量的功能,让这些内部变量的值始终可以保存在内存中。所以可以说闭包是外部使用函数内部的工具。

举个例子:

js 复制代码
function fn1() {
    var a = 1;
    return function(){
    	console.log(a);
    };
}
var res = fn1();
res();  // 1

结合闭包的概念,我们执行这段代码,就可以发现最后输出的结果是 1(即 a 变量的值)。那么可以很清楚地发现,a 变量作为一个 fn1 函数的内部变量,正常情况下作为函数内的局部变量,是无法被外部访问到的。但是通过闭包,我们最后还是可以拿到 a 变量的值。

闭包是如何产生的?

闭包产生的原因跟作用域息息相关,所以你还需要知道什么是作用域链。其实很简单,当访问一个变量时,代码解释器会在当前的作用域查找,如果没找到,就去父级作用域去查找,直到找到该变量或者不存在父级作用域中,这样的链路就是作用域链。

每个子函数都会拷贝上级的作用域,形成作用域链。我们还是用上述代码来解释:

js 复制代码
function fn1() {
    var a = 1;
    return function fn2(){
    	console.log(a);
    };
}
var res = fn1();
res();  // 1
  • 通过作用域的概念,fn1 函数的作用域指向全局作用域(window)和它自己本身;fn2 函数的作用域指向全局作用域 (window)、fn1 和它本身;而作用域是从最底层向上找,直到找到全局作用域 window 为止,如果全局还没有的话就会报错。
  • 那么这就很形象地说明了什么是作用域链,即当前函数一般都会保存上层函数的作用域的引用,从而形成一条作用域链。
  • 由此可见,闭包产生的本质就是:当前环境中存在指向父级作用域的引用

这也就是为什么上述代码能够正确打印出 a 变量的值的原因。因为在当前环境中存在 fn2 -> fn1 -> 全局的作用域链,所以能够找到 a 变量。

是不是只有返回函数才算是产生了闭包呢?其实也不是,回到闭包的本质,我们只需要让父级作用域的引用存在即可,所以也可以将代码修改为:

js 复制代码
var res;
function fn1() {
    var a = 1;
    res =  function(){
    	console.log(a);
    };
}
fn1();
res();  // 1

可以看到输出的结果并没有改变,所以只要当前环境仍然存在指向某个作用域的引用,那就可以获取到该作用域上的变量。

闭包的用途

  1. 在外部访问到函数内部的变量,可以使用这种方法来创建私有变量。

  2. 使已经运行结束的函数上下文中的变量对象继续留在内存中,因为闭包函数保留了这个变量对象的引用,所以这个变量对象不会被回收。

众所周知,原始数据类型是保存在栈空间中的,在上述函数中 fn1 销毁的情况下,我们如何访问到 a 变量的值的呢?

js 复制代码
function fn1() {
    var a = 1;
    return function fn2(){
    	console.log(a);
    };
}
fn1();
var res = fn1();
console.log(res.prototype);

我们将 res.prototype 打印出来:

上图红框的位置我们能看到一个内部的对象 [[Scopes]],其中存放着变量 a,该对象是被存放在堆上的,其中包含了闭包、全局对象等等内容,因此我们能通过闭包访问到本该销毁的变量。

处处可见的闭包

在定时器、事件监听、Ajax 请求、Web Workers 或者任何异步中,只要使用了回调函数,实际上就是在使用闭包。

举个比较经典的例子:

js 复制代码
for(var i = 1; i <= 5; i ++){
  setTimeout(function() {
    console.log(i)
  }, 0)
} 

上述代码执行后,答案是连续输出 5 个 6,一道很经典的面试题,如何解释能让面试官比较满意呢:

  • setTimeout 作为宏任务,由于 JS 中单线程 eventLoop 机制,在主线程同步任务执行完后才去执行宏任务,因此循环结束后 setTimeout 中的回调才依次执行
  • 因为 setTimeout 函数也是一种闭包,往上找它的父级作用域链就是 window变量 i 为 window 上的全局变量,开始执行 setTimeout 之前变量 i 已经就是 6 了,因此最后输出的连续就都是 6。

模拟真实场景

需求:输入框,根据输入的内容及时发送请求(请求下拉框的数据)

简陋的封装一下:

js 复制代码
<div><input type="text"></div>

const ip = document.querySelector('input');
  ip.addEventListener('input', (e) => {
  // 使用 setTimeout 模拟请求
    setTimeout(() => {
      console.log(e.target.value);
    }, 0)
  })

如图:

这时我们将输入 1 的时候做一个延迟效果,模拟第一个数据包最慢响应的场景

js 复制代码
ip.addEventListener('input', (e) => {
    if(e.target.value == 1) {
      setTimeout(() => {
      console.log(1);
    }, 1000);
    }
    else {
      setTimeout(() => {
      console.log(e.target.value);
    }, 0)
    }
})

如图:

在这种情况下,下拉框展示的应该是发送 1 的时候的数据,而不是 111 的数据,应该如何改变呢?是不是就可以利用闭包的特性来修复这个bug:

js 复制代码
ip.addEventListener('input', (e) => {
    const queryData = e.target.value;
    if(e.target.value == 1) {
      setTimeout(() => {
        if(e.target.value == queryData) console.log(queryData);
      }, 1000);
    }
    else {
      setTimeout(() => {
        if(e.target.value == queryData) console.log(queryData);
    }, 0)
    }
})

效果如图:

当然真实的场景可能会复杂得多,但是充分利用好闭包的特性,也许能够帮助你解决很多问题。

总结

本篇主要从 什么是闭包 -> 产生的原因 -> 用途 -> 实际场景 介绍了闭包,也希望能够帮助大家了解闭包,有什么问题也请及时指出。

相关推荐
naice31 分钟前
我对github的图片很不爽了,于是用AI写了一个图片预览插件
前端·javascript·git
天蓝色的鱼鱼35 分钟前
Element UI 2.X 主题定制完整指南:解决官方工具失效的实战方案
前端·vue.js
RoyLin40 分钟前
TypeScript设计模式:门面模式
前端·后端·typescript
小奋斗43 分钟前
千量数据级别的数据统计分析渲染
前端·javascript
小文刀6961 小时前
CSS-响应式布局
前端
三小河1 小时前
overflow:auto 滚动的问题,以及flex 布局中如何设置
前端·javascript
薛定谔的算法1 小时前
phoneGPT:构建专业领域的检索增强型智能问答系统
前端·数据库·后端
Hilaku1 小时前
Token已过期,我是如何实现无感刷新Token的?
前端·javascript·面试
小文刀6961 小时前
2025-35st-w-日常开发总结
前端
我是日安1 小时前
从零到一打造 Vue3 响应式系统 Day 8 - Effect:深入剖析嵌套 effect
前端·vue.js