【面试题】对闭包的理解?什么是闭包?

news2024/11/23 17:01:16

大厂面试题分享 面试题库

后端面试题库 (面试必备) 推荐:★★★★★

地址:前端面试题库

闭包的背景

由于js中只有两种作用域,全局作用域和函数作用域,而在开发场景下,将变量暴露在全局作用域下的时候,是一件非常危险的事情,特别是在团队协同开发的时候,变量的值会被无意篡改,并且极难调试分析。这样的情况下,闭包将变量封装在局部的函数作用域中,是一种非常合适的做法,这样规避掉了被其他代码干扰的情况。

闭包的使用

下面是一种最简单直接的闭包示例

//妈妈本体functionmother(){
    //口袋里的总钱数let money = 100//消费行为returnfunction (pay){
        //返回剩余钱数return money - pay
    }
}
//为儿子消费let payForSon = mother()
//打印最后的剩余钱数console.log(payForSon(5))
复制代码

为了便于理解,我们将外部函数比喻为妈妈本体,里面保存着总钱数这个变量和消费这个行为,通过创建为儿子消费的这个行为对象,然后执行这个行为花费5元,返回剩余的95元。

这个就是为了将变量money保存在mother本体内而避免暴露在外部的全局环境作用域中,只能通过mother()创建消费行为来影响money这个变量。

由此可以归纳总结使用闭包的三个步骤

  1. 用外层函数包裹变量,函数;

  1. 外层函数返回内层函数;

  1. 外部用变量保存外部函数返回的内层函数

目的是为了形成一个专属的变量,只在专属的作用域中操作。

上述的闭包代码示例中,有一个缺陷的场景是,在后续不需要money变量的情况下,没有释放该变量,造成内存泄露。原因是payForSon这个函数的作用域链引用着money对象,解决的办法是将payForSon = null就可以释放方法作用域,进而解除对money的引用,最后释放money变量。

闭包的扩展

函数柯里化


在开发的场景中,有时需要通过闭包来实现函数的柯里化调用。调用示例如下

alert(add(1)(2)(3))
复制代码

这种连续的传参调用函数,叫做函数柯里化。

通过闭包的实现方式如下

functionadd(a){
    //保存第一个参数let sum = a
    functiontmp(b){
        //从第二个函数开始递加
        sum = sum + b
        //返回tmp,让后续可以继续传参执行return tmp
    }
    tmp.toString = function(){
        return sum
    }
    //返回加法函数return tmp
}
alert(add(1)(2)(3))
复制代码

下面我们来一步步分析,

  1. add(1)执行时,保存第一个参数到sum变量中,返回tmp函数

  1. add(1)(2)执行等于tmp(2),将2的值加到了变量sum上,返回tmp函数本身

  1. add(1)(2)(3)执行等同于上述步骤的加到比变量sum上,返回tmp函数本身

  1. alert(add(1)(2)(3))执行时,alert需要将值转为string显示,最后的tmp函数执行tmp.toString,返回sum的值。

矩阵点击应用


该例子的demo代码在我的github上,可以自行取阅

需求:在一个4*4的矩阵方块中,实现点击每个按钮时记录下各自的点击次数,相互之间互不干扰。

思路:在按钮事件中使用闭包,创建独立的存储变量空间。

注意:下列的方案1到方案3是逐次演进的优化方案,需要按照方案标号的次序逐层理解,更有利于理解最终的优化方案

方案1

<div id="container"></div>
...
let container = document.getElementById('container')
for (let r = 0; r < arr.length; r++) {
    for (let c = 0; c < arr[r].length; c++) {
        let cell = document.createElement('div')
        cell.innerHTML = `(${r},${c})`
        container.append(cell)
        cell.onclick = (function () {
            let n = 0
            return function () {
                n++
                cell.innerHTML = `点${n}`
            }
        })()
    }
}
复制代码

在每个按钮上通过onclick绑定闭包方法,存储操作独立的n变量,这样就可以单独记录每个按钮的点击次数

缺点:这样做有一个不足的地方是,外部无法获取内部的n变量,不能实现与外部的交互,比如按钮间的相互影响。

方案2

为了改善方案1的缺点,我们引入外部数据arr来操作管控按钮点击数。 代码示例如下:

let arr = [
            [0, 0, 0, 0],
            [0, 0, 0, 0],
            [0, 0, 0, 0],
            [0, 0, 0, 0],
        ]
let container = document.getElementById('container')
for (let r = 0; r < arr.length; r++) {
    for (let c = 0; c < arr[r].length; c++) {
        let cell = document.createElement('div')
        cell.innerHTML = `(${r},${c})`
        container.append(cell)
        cell.onclick = (function (r, c) {
            return function () {
                arr[r][c]++
                cell.innerHTML = `点${arr[r][c]}`
            }
        })(r, c)
    }
}
复制代码

参照方案1 ,改动点包含两个

  • 新增arr二维数组来记录点击数,这样可以达到与外部交互的目的

  • onclick绑定的事件新增r,c两个参数,并且执行时传参进入,这样就可以把行列参数传递到方法内部(onclick的执行环境作用域与r,c所在的环境不一致,所以无法直接使用)

这样改进完以后,外部可以通过操作arr来与每个按钮的点击次数进行交互。

缺点:这样会将arr暴露在全局作用域下(可以在console控制台访问到),很容易被其他人或者模块误操作,也不利于封装

方案3

基于方案2的改进实现为,用一个立即执行的函数包裹住整个执行代码,这样就构建了一个函数作用域来封装arr变量为私有。代码如下:

(function () {
        let arr = [
            [0, 0, 0, 0],
            [0, 0, 0, 0],
            [0, 0, 0, 0],
            [0, 0, 0, 0],
        ]
        let container = document.getElementById('container')
        for (let r = 0; r < arr.length; r++) {
            for (let c = 0; c < arr[r].length; c++) {
                let cell = document.createElement('div')
                cell.innerHTML = `(${r},${c})`
                container.append(cell)
                cell.onclick = (function (r, c) {
                    return function () {
                        arr[r][c]++
                        cell.innerHTML = `点${arr[r][c]}`
                    }
                })(r, c)
            }
        }
    })()
复制代码

这样一个相对完整的按钮点击次数的方案就完成了。

使用call实现bind


这个需要有call和bind的使用知识的前提,可以自行百度哈

废话不多说,直接上代码

Function.prototype.bind = function(obj){
    console.log('调用自定义bind函数');
    //保存当前函数对象let fun = this//去除第一个obj参数,并且转换为js数组let outerArg = Array.prototype.slice.call(arguments,1)
    returnfunction(){
        //将arguments转为js数组let innerArg = Array.prototype.slice.call(arguments)
        //汇总所有参数let totalArg = outerArg.concat(innerArg)
        //调用外部保存的函数,并且传参
        fun.call(obj,...totalArg)
    }
}

//调用示例let zhangsan = {name:'wawawa'}
functiontotal(s1,s2){
    console.log(this.name + s1 + s2);
}
let bindTotal = total.bind(zhangsan,100)
bindTotal(200)
复制代码

重写函数类的bind函数,

  1. 先将函数对象(也就是下面示例中的total函数)保存在fun变量中,等于闭包外层保存了fun,obj以及其他绑定的参数(由于arguments是类数组对象,需要转换为数组,且去除第一个函数obj);

  1. 然后返回匿名函数,在匿名函数中,将外部和内部的参数进行转换和拼接;

  1. 最后通过fun.call(obj,...totalArg),调用保存的函数对象fun,并且通过call来实现传递绑定的作用域obj,和其他参数totalArg

注意:

  • arguments是类数组对象,不能直接使用数组方法,需要转化为数组操作

  • 外层函数arguments转化时,需要剔除掉obj,因为下面的fun.call需要单独传递obj作为函数作用域

  • totalArg传递给call函数时,需要通过...语法糖摊开数组

大厂面试题分享 面试题库

后端面试题库 (面试必备) 推荐:★★★★★

地址:前端面试题库

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/337388.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

云盘满了怎么办?阿里云服务器云盘扩容操作了解一下

1.背景 2.确定扩容云盘类型与控制台操作 3.ECS实例内部扩容操作说明 3.1 ECS实例内部执行扩容分区 3.2 ECS实例内部执行扩容文件系统 1.背景软件应用的数据库所在服务器磁盘使用率已经达到97%,服务器操作实例如下: 一旦使用达到上限,所有的数…

python基于django的 大学生健康管理系统

随着时代的发展,大学生的数量与日预增但是相对的也出现了很多心理问题,大学生因为各类心理引发的社会问题已经受到了很多人的关注,所以如何更好的培养大学生正确的心理健康问题是现在很多大学多面临的一个重要的问题。 系统设置了三种身份的登录,包括管理员,医生和学生。其中管…

SpringBoot 实现 Excel 导入导出,百万数据量,性能爆表!

最近我做过一个MySQL百万级别数据的excel导出功能&#xff0c;已经正常上线使用了。这个功能挺有意思的&#xff0c;里面需要注意的细节还真不少&#xff0c;现在拿出来跟大家分享一下&#xff0c;希望对你会有所帮助。原始需求&#xff1a;用户在UI界面上点击全部导出按钮&…

最近面试了一位5年的测试,一问三不知,还反怼我...

最近看了很多简历&#xff0c;很多候选人年限不小&#xff0c;但是做的都是一些非常传统的项目&#xff0c;想着也不能通过简历就直接否定一个人&#xff0c;何况现在大环境越来 越难&#xff0c;大家找工作也不容易&#xff0c;于是就打算见一见。 在沟通中发现&#xff0c;由…

ISYSTEM调试实践8-winIDEA Analyzer功能1

前面几篇介绍了ISYSTEM的基本调试界面和功能&#xff0c;相比我之前用过的IDE&#xff0c;除了几种断点方式和脚本功能以外&#xff0c;应该都是比较简单&#xff0c;稍微操作一下就可以直接上手&#xff0c;后续我将介绍winIDEA的Analyzer 功能。 1 Analyzer简介 iSYSTEM An…

一文带你为PySide6编译MySQL插件驱动

1.概述 最近使用PySide6开发程序&#xff0c;涉及与MySQL的数据交互。但是qt官方自pyqt5.12&#xff08;记不太清了&#xff09;以后不再提供MySQL的插件驱动&#xff0c;只能自己根据qt的源码编译。不过网上大部分都是qt5的MySQL驱动的编译教程。后来搜到了一个qt6的编译教程…

为什么我需要一个网站?必须拥有网站的 5 个理由

当社交媒体似乎统治了互联网时&#xff0c;您可能会问自己&#xff1a;“我需要一个网站吗&#xff1f;” 答案是肯定的&#xff0c;我在这里告诉你为什么。2019 年&#xff0c;70-80% 的人在访问/购买之前会在线研究公司&#xff0c;同样比例的客户可能会流向没有网站的小企业…

vlookup怎么用详细步骤,看这一篇就够了

1、vlookup函数&#xff1a;使用方法 以下便是vlookup函数&#xff0c;功能、语法和参数用法&#xff1a; excel函数vlookup 2、vlookup函数&#xff1a;查询参数 首先&#xff0c;选中F2单元格&#xff0c;然后在编辑栏输入函数公式&#xff1a;VLOOKUP(E2&#xff0c;B&…

JVM - 认识JVM规范

目录 重识JVM JVM规范作用及其核心 JVM 整体组成 理解ClassFile结构 ASM开发 重识JVM JVM概述JVM&#xff1a; Java Virtual Machine&#xff0c;也就是Java虚拟机所谓虚拟机是指&#xff1a; 通过软件模拟的具有完整硬件系统功能的、运行在一个完全隔离环境中的计算机系统…

InstructGPT

文章目录Abstract 给定人类的命令&#xff0c;并且用人工标注想要的结果&#xff0c;构成数据集&#xff0c;使用监督学习来微调GPT-3。 然后&#xff0c;我们对模型输出进行排名&#xff0c;构成新的数据集&#xff0c;我们利用强化学习来进一步微调这个监督模型。 我们把产…

day35|01背包问题、416. 分割等和子集

01背包问题 有n件物品和一个最多能背重量为w的背包。第i件物品的重量是weight[i]&#xff0c;得到的价值是value[i] 。每件物品只能用一次&#xff0c;求解将哪些物品装入背包里物品价值总和最大。 例&#xff1a;背包最大重量为4。 物品为&#xff1a; 重量价值物品0115物品…

3种方法设置PDF“打开密码”,总有一种适合你

PDF文件是我们工作中经常用到的文件之一&#xff0c;对于重要的文件&#xff0c;设置“打开密码”是一种很好的保护方式。下面就来说说&#xff0c;设置PDF“打开密码”有哪三种方法&#xff1f; 方法一&#xff1a;在线网站加密 市面上有很多可以直接在线上加密PDF文件的产品…

Cortex-M0编程入门

目录1.嵌入式系统编程入门微控制器是如何启动的嵌入式程序设计2.输入和输出3.开发流程4.C编程和汇编编程5.什么是程序映像6.C编程&#xff1a;数据类型7.用C语言操作外设8.Cortex微控制器软件接口标准&#xff08;CMSIS&#xff09;简介标准化内容组织结构使用方法优势1.嵌入式…

综合项目 旅游网 【4.旅游线路名称查询-参数传递】

目录 查询参数的传递 route_list.html4.0代码 header.html2.0代码 修改后台代码 RouteServlet之前 RouteDaoImpl1.0 改进后的RouteDaoImpl2.0 改进后的RouteServlet2.0 ​编辑修改前台代码 route_list.html RouteServlet3.0 旅游线路的详情展示 分析 整体过程 代码实现…

【遇见青山】项目难点:缓存穿透的解决方案

【遇见青山】项目难点&#xff1a;缓存穿透的解决方案1.缓存穿透现象缓存空对象布隆过滤其他方案2.解决方案&#xff0c;缓存空数据1.缓存穿透现象 缓存穿透是指客户端请求的数据在缓存中和数据库中都不存在&#xff0c;这样缓存永远不会生效&#xff0c;这些请求都会打到数据…

常见的内存操作函数

&#x1f466;个人主页&#xff1a;Weraphael ✍&#x1f3fb;作者简介&#xff1a;目前是C语言学习者 ✈️专栏&#xff1a;C语言航路 &#x1f40b; 希望大家多多支持&#xff0c;咱一起进步&#xff01;&#x1f601; 如果文章对你有帮助的话 欢迎 评论&#x1f4ac; 点赞&a…

Java的异常处理

异常 异常就是程序非正常运行时的报错&#xff0c;不正常就是异常。 异常分类 通常分为两类&#xff1a; Error&#xff1a;错误。通常是Java虚拟机无法解决的严重问题。如&#xff1a;JVM系统内部错误、资源耗尽等严重情况。比如&#xff1a;StackOverflowError和OOM-->…

canvas根据坐标点位画图形-canvas拖拽编辑单个图形形状

首先在选中图形的时候需要用鼠标右击来弹出选择框&#xff0c;实现第一个编辑节点功能 在components文件夹下新建右键菜单 RightMenu文件&#xff1a; <template><div v-show"show" class"right-menu" :style"top:this.ypx;left:this.xpx…

PCB设计中的数据单位,层作用,工艺要求,制作过程等简介

PCB中常识 物理结构常识 几乎每层板子都是由两个金属层夹着一个绝缘层&#xff0c;像一个三明治结构。多层板是这种双层板叠加压合的。 常见的层的作用 参考&#xff1a;https://www.21ic.com/article/854059.html PCB电路板一般而言会分成六层&#xff0c;具体为&#x…

第二十四章 java并发常见知识内容(AQS)

JAVA重要知识点AQS介绍AQS的核心思想AQS资源共享方式自定义同步器常见同步工具类Semaphore(信号量)CountDownLatch(倒计时器)CyclicBarrier(循环栅栏)AQS 介绍 AQS 的全称为 AbstractQueuedSynchronizer &#xff0c;翻译过来的意思就是抽象队列同步器。这个类在 java.util.c…