在 JavaScript 中,闭包(Closure)和高阶函数(Higher-order Functions)都是函数式编程的重要概念,它们在多种编程范式中起着核心作用,尤其是在使代码更灵活、模块化和可重用方面。
函数闭包
在讲高级函数之前不得不讲一下函数的闭包,我们所说的高级函数都是基于函数的闭包封装好的函数。
闭包
对于闭包的定义: 闭包是指有权访问另外一个函数作用域中的变量的函数
闭包延长了生命域周期,且外部是无法对闭包里面缓存的值进行直接访问的。通俗来说,在闭包函数里有一个可以缓存的有独立作用域的函数。我们简单举个例子:
function demo(){
let number = 0
return function(){
console.log(number++)
}
}
let f1 = demo()
f1()
f1()
f1()
// console.log(number)
运行结果:
其中我创建一个了一个函数demo返回了一个函数。当我每次执行f1的时候,输出的结果都是demo函数中的number。解释一下刚刚我说的闭包的特点:
- 可缓存:就在这个例子中可以很明确的看到输出的值并不像普通的函数一样,每次执行函数输出的结果都是一样的;而是在闭包函数里面,记录着上一次的执行状态/结果。也就是将数据进行了缓存
- 独立作用域:如果将我最后一行代码取消注释的话,很明显是访问不到demo函数里面的number值的,这个是由函数的特性决定的
高级函数入门
高阶函数是指那些接受函数作为参数或将函数作为返回结果的函数。这种函数的存在使得 JavaScript 程序可以将函数作为模块单元,传递和返回,从而提高代码的复用性和抽象性。
这个概念很抽象,简单点说和闭包的去比诶就是,高级函数的入参是函数,返回的也是一个参数。下面就是一个非常简单的例子,让我们认识一下高级函数,好介绍下一个部分的常用的高级函数。
function fun(msg){
console.log(`log - ${msg}`)
return this.helloMsg
}
//fn是一个函数
function fun2(fn){
this.helloMsg = "this.helloMsg"
//...agr接收的是fn传入的参数
return function (...agr){
//显示将fun2中的作用域显示绑定this给fn
//执行函数,ans就是fn的返回值(如果有返回值的话)
let ans = fn.apply(this, agr)
return ans
}
}
let f = fun2(fun)
console.log(f("msg"))
输出结果:
fun就是一个简单的函数,输入一个字符串打印出来,然后返回一个字符串。
fun2 是封装的一个最简单的高级函数。这个fun2的入参是一个函数,然后执行fn,返回结果。对于封装fun2:
- fn:入参是一个函数(不是返回结果)
- apply:显示绑定this并且执行函数。
-
- this绑定:绑定的这个this是fun2中this,也就是说在fn中可以使用fun2的上下文
- agr:上面也说了,fun2的入参是一个函数,而不是函数结果。如果执行fn需要参数(如f("msg")中的"msg"),就是由fun2中返回的函数接收,并且使用...这个扩展运算符,接受从零到多个参数,并且将它们作为一个数组处理
调用函数:
- f: 通过调用
fun2(fun)得到的结果,它实际上是fun2返回的那个新函数的一个实例。这个新函数保留了对fun和fun2中定义的变量的访问,这是通过 JavaScript 的闭包机制实现的。 - fun - 函数 + fun2 - 含有闭包的函数 = f - 函数 (之所以会得到一个函数因为fun2返回的就是一个匿名函数,在这个匿名函数中会执行fun)。f这个函数的执行会间接的执行fun函数,并且使用的是有fun2上下文的作用域
上面这个高级函数其实并没有解决什么实际上的问题,只是想通过这个例子来引入这么个高级函数的模板。
高级函数的清除/创建
前面也说了,高级函数其中的闭包部分可以让我们缓存数据,就比如说demo里面的number,假如需要重置或者说清空这个缓存呢?当然你直接let f2 =demo()直接创建一个新的函数实例也可以,创建了一个新的缓存。
要“删除”已有的缓存,直接将已有的缓存置空f1 = null,就可以完成任务。
常用高级函数
Once
使用场景:在封装一些删除的按钮的时候,经常会出现一个问题:会因为时延或者组件本身有动画,导致用户点击与删除提示不同步(我点击一下没反应,然后多点击几遍)。但是除了第一次之外,后续的点击其实删除的是空对象,这样的行为会出现很多奇怪的bug。
我们封装Once函数就可以实现,通过高级函数Once调用的fn函数只会在函数实例中执行一次。
function Once(fn){
return function (...agr){
//如果是第一次执行fn并没有被置空,继续执行
if(fn){
let ans = fn.apply(this, ...agr);
// 执行过一次之后将fn置空,这样在Once的作用域中fn之后一直都将是null
fn = null;
return ans;
}
}
}
运行结果:
解析
在创建实例deleteFunction之后,在作用域内,fn就是创建实例时传入的函数fun。当deleteFunction第一次被执行时,fn是可以正常被执行的。在执行完之后,我们要完成“第一次执行之后都不执行”这个需求,我们就可以在Once的作用域内将fn指针指向null。
当第二次或者更多次被执行时,在deleteFunction作用域内fn = null,所以不会执行fun。
throttle(节流函数)
使用场景:可以说是说有的按钮,应该都有这个功能:一段时间内,多次点击,只执行一次。
有了Once函数的基础,我们直接说一下throttle的核心是啥:最近一次目标函数被执行后,在一定的时间内不允许再次执行;用计时器的状态限制目标函数的执行
function debounce(fn,time){
let timer = null
return function(...agr){
if(!timer){
// 第一次执行 or 已经超过了延迟时间,将timer清除后
fn.apply(this, agr)
// 执行函数之后使用计时器,等待延迟时间,将timer清除,再次执行
// 在延迟时间内被调用,timer != null,就不会执行
timer = setTimeout(()=>{
timer = null;
},time)
}
}
}
// 我们假设登录请求1.5秒内只能发送一次请求
let loginBtnFunction = debounce(fun,1500)
执行结果:
解析
起初在debounce作用域中timer = null,当fn被执行之后,将timer指向一个单次计时器。如果这个计时器不结束,即使你多次调用,在debounce函数的作用域中timer都不为空(指向那个正在计时的计时器);当计时器结束,将timer清除之后,函数再次被调用,就可以再次执行。如第二次输出发送请求。
debounce(防抖函数)
使用场景:在一些自动保存的组件在中比较常见。比如,对于一个在线文本编辑器来说,在用户进行修改之后我需要进行保存,执行保存这个操作的策略最常见的就是定时保存:每五秒保存一次。对于前端来说没什么问题,但是定时提交数据,对于后端服务器而言,那就是问题很大了,如果用户真的没五秒钟都进行修改,做了有效操作还好,但是如果用户在这五秒并没有操作呢?
另外一种保存的策略就是,在用户进行修改之后进行保存。如果不加以其他的操作,用户每修改一个内容就想向后端发送数据,服务器更加崩溃(服务器:我可没惹你们,你们为什么要这么对我~/~呜呜)。所以需要使用到debounce这个函数。减少前端操作的频率。
防抖函数的bounce的作用:当被调用不会立刻执行,在n秒内不再被调用,才执行。延伸到上面那个自动保存的例子就是:我修改了文本但是n秒内我再次修改,就不会执行自动保存;如果我进行修改结束之后n秒再保存(如果一直发生改变,不执行目标函数;变化停止,再执行)
// 在构造实例需要传入延迟时间:延迟多久之后执行函数
function throttle(fn,time){
let timer = null
return function(...agr){
if(!timer){
timer = setTimeout(() => {
timer = null;
fn.apply(this,agr)
}, time);
}
}
}
解析
有了上面的Once函数的基础,我们可以直接尝试理解throttle函数的核心在哪:每次被调用都在维持一个定时器的状态。
在throttle的实例创建之初,timer = null然后启动一个计时器进行等待。当等待时间还没有结束时,再次被调用,计时器将被清除,然后再次启动计时器等待。如此反复.....
当最后一次被调用并且计时器结束之后,目标函数才会执行
总结
从上面的高级函数的例子中,我们其实可以看到,高级函数实现的功能都是依靠闭包中的缓存特性,根据上一次执行的结果,来判断下一次函数的执行情况。
- Once():执行过一次之后缓存fn -> null,之后不再执行
- throttle():缓存timer这个计时器,当timer -> null才可以执行;当timer指向一个定时器的时候,就无法再次执行
- debounce():也是缓存timer,只有完整等待timer指向的定时器结束之后才能执行;等待期间再次被调用,只会清空之前等待的时间(重启计时器)
GPT:在函数式编程中,闭包和高阶函数使得编写无副作用的代码(即不改变外部状态的代码)更为简单。它们也有助于实现如延迟计算和函数组合等高级编程技术。这些特性使得代码更加模块化,增加了代码的可测试性和可维护性。