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

前言

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

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

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

闭包到底是什么?

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

举个例子:

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)
    }
})

效果如图:

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

总结

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

相关推荐
秋名山大前端2 小时前
Chrome GPU 加速优化配置(前端 3D 可视化 / 数字孪生专用)
前端·chrome·3d
今天不要写bug3 小时前
antv x6实现封装拖拽流程图配置(适用于工单流程、审批流程应用场景)
前端·typescript·vue·流程图
luquinn3 小时前
实现统一门户登录跳转免登录
开发语言·前端·javascript
用户21411832636023 小时前
dify案例分享-5分钟搭建智能思维导图系统!Dify + MCP工具实战教程
前端
augenstern4163 小时前
HTML(面试)
前端
excel3 小时前
前端常见布局误区:1fr 为什么撑爆了我的容器?
前端
烛阴3 小时前
TypeScript 类型魔法:像遍历对象一样改造你的类型
前端·javascript·typescript
vayy3 小时前
uniapp中 ios端 scroll-view 组件内部子元素z-index失效问题
前端·ios·微信小程序·uni-app
专注API从业者3 小时前
基于 Node.js 的淘宝 API 接口开发:快速构建异步数据采集服务
大数据·前端·数据库·数据挖掘·node.js
前端无冕之王3 小时前
一份兼容多端的HTML邮件模板实践与详解
前端·css·数据库·html