JS闭包无处不在

2024-02-22 10:48
文章标签 js 闭包 无处不在

本文主要是介绍JS闭包无处不在,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

闭包的概念和特性

首先看个闭包的例子:

function makeFab () {let last = 1, current = 1return function inner() {[current, last] = [current + last, current]return last}
}let fab = makeFab()
console.log(fab()) // 1
console.log(fab()) // 2
console.log(fab()) // 3
console.log(fab()) // 5

这是一个生成斐波那契数列的例子。makeFab的返回值就是一个闭包,makeFab像一个工厂函数,每次调用都会创建一个闭包函数,如例子中的fab。

fab每次调用不需要传参数,都会返回不同的值,因为在闭包生成的时候,它记住了变量last和current,以至于在后续的调用中能够返回不同的值。

能记住函数本身所在作用域的变量,这就是闭包和普通函数的区别所在。

MDN中给出的闭包的定义是:函数与对其状态即词法环境的引用共同构成闭包。

这里的“词法环境的引用”,可以简单理解为“引用了函数外部的一些变量”,例如上述例子中每次调用makeFab都会创建并返回inner函数,引用了last和current两个变量。

闭包——函数式编程之魂

JavaScript和python这两门动态语言都强调一个概念:万物皆对象。自然,函数也是对象。
在JavaScript里,我们可以像操作普通变量一样,把函数在我们的代码里抛来抛去,然后在某个时刻调用一下,这就是所谓的函数式编程。

函数式编程灵活简洁,而语言对闭包的支持,让函数式编程拥有了灵魂。

以实现一个可复用的确认框为例,比如在用户进行一些删除或者重要操作的时候,为了防止误操作,我们可能会通过弹窗让用户再次确认操作。

因为确认框是通用的,所以确认框组件的逻辑应该足够抽象,仅仅是负责弹窗、触发确认、触发取消事件,而触发确认/取消事件是异步操作,这时候我们就需要使用两个回调函数完成操作,弹窗函数confirm接收三个参数:一个提示语句,一个确认回调函数,一个取消回调函数:

function confirm (confirmText, confirmCallback, cancelCallback) {// 插入提示框DOM,包含提示语句、确认按钮、取消按钮// 添加确认按钮点击事件,事件函数中做dom清理工作并调用confirmCallback// 添加取消按钮点击事件,事件函数中做dom清理工作并调用cancelCallback
}

这样我们可以通过向confirm传递回调函数,并且根据不同结果完成不同的动作,比如我们根据id删除一条数据可以这样写:

function removeItem (id) {confirm('确认删除吗?', () => {// 用户点击确认, 发送远程ajax请求api.removeItem(id).then(xxx)}, () => {// 用户点击取消,console.log('取消删除')})
}

这个例子中,confirmCallback正是利用了闭包,创建了一个引用了上下文中id变量的函数,这样的例子在回调函数中比比皆是,并且大多数时候引用的变量是很多个。

试想,如果语言不支持闭包,那这些变量要怎么办?作为参数全部传递给confirm函数,然后在调用confirmCallback/cancelCallback时再作为参数传递给它们?显然,这里闭包提供了极大便利。

闭包的一些例子

防抖、节流函数

前端很常见的一个需求是远程搜索,根据用户输入框的内容自动发送ajax请求,然后从后端把搜索结果请求回来。

为了简化用户的操作,有时候我们并不会专门放置一个按钮来点击触发搜索事件,而是直接监听内容的变化来搜索(比如像vue的官网搜索栏)。

这时候为了避免请求过于频繁,我们可能就会用到“防抖”的技巧,即当用户停止输入一段时间(比如500ms)后才执行发送请求。

可以写一个简单的防抖函数实现这个功能:

debounce函数每次调用时,都会创建一个新的闭包函数,该函数保留了对事件逻辑处理函数func以及防抖时间间隔time以及定时器标志timer的引用。

类似的还有节流函数:

function throttle(func, time) {let timer = 0 // 定时器标记相当于一个锁标志return function (...args) {if (timer) returnfunc.apply(this, args)timer = setTimeout(() => timer = 0, time)}
}
优雅解决按钮多次连续点击问题

用户点击一个表单提交按钮,前端会向后台发送一个异步请求,请求还没返回,焦急的用户又多点了几下按钮,造成了额外的请求。

有时候多发几次请求最多只是多消耗了一些服务器资源,而另外一些情况是,表单提交本身会修改后台的数据,那多次提交就会导致意料之外的后果了。

无论是为了减少服务器资源消耗还是避免多次修改后台数据,给表单提交按钮添加点击限制是很有必要的。

怎么解决呢?一个常用的办法是打个标记,即在响应函数所在作用域声明一个布尔变量lock,响应函数被调用时,先判断lock的值,为true则表示上一次请求还未返回,此次点击无效;为false则将lock设置为true,然后发送请求,请求结束再将lock改为false。

很显然,这个lock会污染函数所在的作用域,比如在vue组件中,我们可能就要将这个标记记录在组件属性上;而当有多个这样的按钮,则还需要不同的属性来标记(想想给这些属性取名都是一件头疼的事情吧!)。

而生成闭包伴随着新的函数作用域的创建,利用这一点,刚好可以解决这个问题。下面是一个简单的例子:

let clickButton = (function () {let lock = falsereturn function (postParams) {if (lock) returnlock = true// 使用axios发送请求axios.post('urlxxx', postParams).then(// 表单提交成功).catch(error => {// 表单提交出错console.log(error)}).finally(() => {// 不管成功失败 都解锁lock = false})}
})()
button.addEventListener('click', clickButton)

这样lock变量就会在一个单独的作用域里,一次点击的请求发出以后,必须等请求回来,才会开始下一次请求。

当然,为了避免各个地方都声明lock,修改lock,我们可以把上述逻辑抽象一下,实现一个装饰器,就像节流/防抖函数一样。以下是一个通用的装饰器函数:

function singleClick(func, manuDone = false) {let lock = falsereturn function (...args) {if (lock) returnlock = truelet done = () => lock = falseif (manuDone) return func.call(this, ...args, done)let promise = func.call(this, ...args)promise ? promise.finally(done) : done()return promise}
}

默认情况下,需要原函数返回一个promise以达到promise决议后将lock重置为false,而如果没有返回值,lock将会被立即重置(比如表单验证不通过,响应函数直接返回),调用示例:


let clickButton = singleClick(function (postParams) {if (!checkForm()) returnreturn axios.post('urlxxx', postParams).then(// 表单提交成功).catch(error => {// 表单提交出错console.log(error)})
})
button.addEventListener('click', clickButton)

在一些不方便返回promise或者请求结束还要进行其它动作之后才能重置lock的地方,singleClick提供了第二个参数manuDone,允许你可以手动调用一个done函数来重置lock,这个done函数会放在原函数参数列表的末尾。使用例子:

let print = singleClick(function (i, done) {console.log('print is called', i)setTimeout(done, 2000)
}, true)function test () {for (let i = 0; i < 10; i++) {setTimeout(() => {print(i)}, i * 1000)}
}

print函数使用singleClick装饰,每次调用2秒后重置lock变量,测试每秒调用一次print函数,执行代码输出如下图:

可以看到,其中一些调用没有打印结果,这正是我们想要的结果!singleClick装饰器比每次设置lock变量要方便许多,这里singleClick函数的返回值,以及其中的done函数,都是一个闭包。

闭包模拟私有方法或者变量

“封装”是面向对象的特性之一,所谓“封装”,即一个对象对外隐藏了其内部的一些属性或者方法的实现细节,外界仅能通过暴露的接口操作该对象。

js是比较“自由”的语言,所以并没有类似C++语言那样提供私有变量或成员函数的定义方式,不过利用闭包,却可以很好地模拟这个特性。

比如游戏开发中,玩家对象身上通常会有一个经验属性,假设为exp,“打怪”、“做任务”、“使用经验书”等都会增加exp这个值,而在升级的时候又会减掉exp的值,把exp直接暴露给各处业务来操作显然是很糟糕的。

在js里面我们可以用闭包把它隐藏起来,简单模拟如下:

function makePlayer () {let exp = 0 // 经验值return {getExp () {return exp},changeExp (delta, sReason = '') {// log(xxx),记录变动日志exp += delta}}
}let p = makePlayer()
console.log(p.getExp()) // 0
p.changeExp(2000)
console.log(p.getExp()) // 2000

这样我们调用makePlayer()就会生成一个玩家对象p,p内通过方法操作exp这个变量,但是却不可以通过p.exp访问,显然更符合“封装”的特性。

四、总结
闭包是js中的强大特性之一,然而至于闭包怎么使用,我觉得不算是一个问题,甚至我们完全没必要研究闭包怎么使用。

我的观点是,闭包应该是自然而言地出现在你的代码里,因为它是解决当前问题最直截了当的办法;而当你刻意想去使用它的时候,往往可能已经走了弯路。

完结撒花,有疑问留言探讨哈~~~

这篇关于JS闭包无处不在的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/735023

相关文章

JS常用组件收集

收集了一些平时遇到的前端比较优秀的组件,方便以后开发的时候查找!!! 函数工具: Lodash 页面固定: stickUp、jQuery.Pin 轮播: unslider、swiper 开关: switch 复选框: icheck 气泡: grumble 隐藏元素: Headroom

在JS中的设计模式的单例模式、策略模式、代理模式、原型模式浅讲

1. 单例模式(Singleton Pattern) 确保一个类只有一个实例,并提供一个全局访问点。 示例代码: class Singleton {constructor() {if (Singleton.instance) {return Singleton.instance;}Singleton.instance = this;this.data = [];}addData(value)

Node.js学习记录(二)

目录 一、express 1、初识express 2、安装express 3、创建并启动web服务器 4、监听 GET&POST 请求、响应内容给客户端 5、获取URL中携带的查询参数 6、获取URL中动态参数 7、静态资源托管 二、工具nodemon 三、express路由 1、express中路由 2、路由的匹配 3、路由模块化 4、路由模块添加前缀 四、中间件

EasyPlayer.js网页H5 Web js播放器能力合集

最近遇到一个需求,要求做一款播放器,发现能力上跟EasyPlayer.js基本一致,满足要求: 需求 功性能 分类 需求描述 功能 预览 分屏模式 单分屏(单屏/全屏) 多分屏(2*2) 多分屏(3*3) 多分屏(4*4) 播放控制 播放(单个或全部) 暂停(暂停时展示最后一帧画面) 停止(单个或全部) 声音控制(开关/音量调节) 主辅码流切换 辅助功能 屏

使用JS/Jquery获得父窗口的几个方法(笔记)

<pre name="code" class="javascript">取父窗口的元素方法:$(selector, window.parent.document);那么你取父窗口的父窗口的元素就可以用:$(selector, window.parent.parent.document);如题: $(selector, window.top.document);//获得顶级窗口里面的元素 $(

js异步提交form表单的解决方案

1.定义异步提交表单的方法 (通用方法) /*** 异步提交form表单* @param options {form:form表单元素,success:执行成功后处理函数}* <span style="color:#ff0000;"><strong>@注意 后台接收参数要解码否则中文会导致乱码 如:URLDecoder.decode(param,"UTF-8")</strong></span>

js react 笔记 2

起因, 目的: 记录一些 js, react, css 1. 生成一个随机的 uuid // 需要先安装 crypto 模块const { randomUUID } = require('crypto');const uuid = randomUUID();console.log(uuid); // 输出类似 '9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d'

学习记录:js算法(二十八):删除排序链表中的重复元素、删除排序链表中的重复元素II

文章目录 删除排序链表中的重复元素我的思路解法一:循环解法二:递归 网上思路 删除排序链表中的重复元素 II我的思路网上思路 总结 删除排序链表中的重复元素 给定一个已排序的链表的头 head , 删除所有重复的元素,使每个元素只出现一次 。返回 已排序的链表 。 图一 图二 示例 1:(图一)输入:head = [1,1,2]输出:[1,2]示例 2:(图

uuid.js 使用

相关代码 import { NIL } from "uuid";/** 验证UUID* 为空 则返回 false* @param uuid* @returns {boolean}*/export function MyUUIDValidate(uuid: any): boolean {if (typeof uuid === "string" && uuid !== NIL) { //uuid

js定位navigator.geolocation

一、简介   html5为window.navigator提供了geolocation属性,用于获取基于浏览器的当前用户地理位置。   window.navigator.geolocation提供了3个方法分别是: void getCurrentPosition(onSuccess,onError,options);//获取用户当前位置int watchCurrentPosition(