一文详解JavaScript闭包典型应用
本文深入解析JavaScript闭包的五大典型应用场景:利用IIFE模拟私有变量、实现函数柯里化与偏函数、以及前端常用的防抖和节流技术。同时澄清了闭包与内存泄漏的关系,指出其并非泄漏根源,并详细剖析了闭包在循环体中的经典考题及三种解决方案,帮助开发者掌握闭包的高级用法。
1.应用场景
下文列举的开发高频使用场景,也是面试中稳妥的回答方向。
1.1 模拟私有变量
JS作为基于对象的语言,强调对象而非类。ES6引入class关键字可模拟类并生成实例。
即便使用class模拟,仍无法实现传统面向对象语言的部分能力,例如私有变量的定义与使用。
通过User类示例了解私有变量(此为伪代码,不可直接运行)。
class User{
constructor(username,password){
// 用户名
this.username = username
// 密码
this.password = password
}
login(){
// 使用axious进行登录请求
axios({
method: 'GET',
url: 'http://127.0.0.1/server',
params: {
username,
password
},
}).then(response => {
console.log(response);
});
}
}User类中定义了属性及login方法,尝试输出password属性。
let user = new User('小明',123456)
user.password // 123465登录密码等敏感信息若可通过简单属性获取,意味着任何持有user对象的人均可轻松获取或篡改密码。实际业务中此举极具风险,需从代码层面保护password。
此类变量仅希望在函数或对象内部方法中访问,外部不可触及。此类变量即私有变量,通常以_或双_开头定义。
借助闭包可在类中声明变量私有性。思路是将私有变量置于最外层立即执行函数中,并通过立即执行User函数,创造闭包作用域环境。
// 利用IIFE生成闭包,返回user类
const User = (function () {
// 定义私有变量_password
let _password
class User {
constructor(username, password) {
// 初始化私有变量_password
_password = password
this.username = username
}
login() {
console.log(this.username, _password)
}
}
return User
})()
let user = new User('小明',123465)
console.log(user.username); // 小明
console.log(user.password); // undefined
console.log(user._password); //undefined
user.login(); // 小明 undefined代码中,私有变量_password被妥善保护在User立即执行函数内部。此时实例暴露属性已无_password,通过闭包成功利用自由变量模拟私有变量效果。
1.2 柯里化
定义一个返回函数的函数。柯里化是将接收n个参数的单个函数改造为只接收1个参数的n个互相嵌套函数的过程,即从fn(a,b,c)变为fn(a)(b)(c)。
通过案例深入理解:以慕课网为例,使用site(站点)、type(课程类型)、name(课程名称)三个字符串拼接生成课程完整版名称。对应方法如下:
function generateName(site,type,name){
return site + type + name
}该函数需传递三个参数。若我是课程运营负责人,仅负责“体系课”业务,每次生成课程时均固定传参site,操作如下:
generateName('体系课',type,name)若我是细分工种的前端助教,仅负责“体系课”站点下的“前端”课程,传参方式如下:
generateName('体系课','前端',name)不难发现,调用generateName时,真正变量仅有一个,但每次不得不手动传递前两个参数。此时柯里化登场,它可帮助我们在必要时记住部分参数。
function generateName(site){
// var site = '体系课'
return function(type){
// var type = '前端'
return function(name){
// var name = '零基础就业班'
return prefix + type + name
}
}
}
// 生成体系课专属函数
var salesName = generateName('体系课');
// “记住”site,生成体系课前端课程专属函数
var salesBabyName = salesName('前端')
// 输出 '体系课前端零基础就业班'
res = salesBabyName('零基础就业班')
console.log(res)在生成体系课专属函数中,将site作为实参传递给generateName,并将site值保留在generateName内部作用域中。
在生成体系课前端课程函数中,将type值保留在salesBabyName函数中,最终调用salesBabyName函数并输出。
如此,原有的generateName (site, type, name)函数经柯里化变为generateName(site)(type)(name)。通过后者形式,可记住部分形参,选择性传递参数,从而编写出更符合预期、复用性更高的函数。
function generateName(site){
// var site = '实战课'
return function(type){
// var type = 'Java'
return function(name){
// var name = '零基础'
return site + type + name
}
}
}
// "记住“site和type,生成实战课java专属函数
var shiZhanName = generateName('实战课')('Java')
console.log(shiZhanName);
// 输出 '实战课java零基础'
var res = shiZhanName('零基础')
console.log(res)
// 啥也不记,直接生成一个完整课程
var itemFullName = generateName('实战课')('大数据')('零基础')
console.log(itemFullName);1.3 偏函数
偏函数与柯里化类似,理解柯里化后,偏函数便易如反掌。
柯里化将n个参数的函数转化为n个单参数函数。若有三个入参,需嵌套三层函数,且每层仅一个入参。柯里化目标是将函数拆解为精准的n部分。
偏函数相对随意,其固定函数中的某一个或几个参数,返回新函数。若有三个入参,可只固定一个,返回另一个入参函数。即偏函数应用不强调“单参数”概念。
仍以上例,原函数形式调用:
function generateName(site,type,name){
return site + type + name;
}
// 调用时传入三个参数
var itemFullName = generateName('体系课', '前端', '2022')偏函数改造:
function generateName(site){
return function(type,name){
return site + type + name
}
}
// 把3个参数分两部分传入
var itemFullName = generateName('体系课')('前端', '2022')1.4 防抖
浏览器中scroll、resize、鼠标事件(如mousemove、mouseover)、键盘事件(如keyup、keydown)等易频繁触发。频繁触发回调导致大量计算,引发页面抖动或卡顿,影响性能。防抖和节流是控制事件触发频率的两种手段。
防抖核心思想:在特定时间段内,无论触发多少次回调,仅执行最后一次。
// fn是我们需要包装的事件回调, delay是每次推迟执行的等待时间
function debounce(fn, delay) {
// 定时器
let timer = null
// 将debounce处理结果当作函数返回
return function () {
// 保留调用时的this上下文
let context = this
// 保留调用时传入的参数
let args = arguments
// 每次事件被触发时,都去清除之前的旧定时器
if(timer) {
clearTimeout(timer)
}
// 设立新定时器
timer = setTimeout(function () {
fn.apply(context, args)
}, delay)
}
}
// 用debounce来包装scroll的回调
const better_scroll = debounce(() => console.log('触发了滚动事件'), 1000)
document.addEventListener('scroll', better_scroll)1.5 节流
节流核心思想:在特定时间段内,无论触发多少次回调,仅认可第一次,并在计时结束时响应,即隔一段时间执行一次。
// fn是我们需要包装的事件回调, interval是时间间隔的阈值
function throttle(fn, interval) {
// last为上一次触发回调的时间
let last = 0
// 将throttle处理结果当作函数返回
return function () {
// 保留调用时的this上下文
let context = this
// 保留调用时传入的参数
let args = arguments
// 记录本次触发回调的时间
let now = +new Date()
// 判断上次触发的时间和本次触发的时间差是否小于时间间隔的阈值
if (now - last >= interval) {
// 如果时间间隔大于我们设定的时间间隔阈值,则执行回调
last = now;
fn.apply(context, args);
}
}
}
// 用throttle来包装scroll的回调
const better_scroll = throttle(() => console.log('触发了滚动事件'), 1000)
document.addEventListener('scroll', better_scroll)2.性能问题
前述讲解了闭包常见应用,可见其特性强大,但人们对其存在诸多误解。一种耸人听闻的说法是闭包会造成内存泄露,故应尽量减少使用。事实果真如此吗?
2.1 内存泄漏
该释放的变量未释放,仍占据内存空间,导致内存占用攀升,引发性能恶化、系统崩溃等问题,此现象称为内存泄漏。
闭包中的变量是我们需要的变量,本就不该释放,何来内存泄漏之说?
因此,有关内存泄露问题属谣言,系误传。
此误传源于IE。IE在使用完闭包后,仍无法释放内部变量,这是IE的bug,而非闭包问题。
若仍不放心,请看以下示例:
function f1(){
var num = Math.randon();
function f2(){
return num
}
return f2
}
var f = f1();
f();上述代码中,f2函数存在对变量num的引用,故num变量不会回收。我们可在函数调用后,将外部引用关系置空,如下:
function f1(){
var num = Math.randon();
function f2(){
return num
}
return f2
}
var f = f1();
f();
f = null;事实上,闭包导致内存泄漏属误传。闭包中引用的变量,相当于全局变量,并不构成内存泄漏问题。内存泄漏多由代码不规范导致。
2.2 常见的内存泄漏
2.21 不必要的全局变量
function f1() {
name = '小明'
}非严格模式下引用未声明变量,会在全局对象中创建新变量。浏览器中全局对象为window,这意味着name变量将泄漏至全局。全局变量仅在网页关闭时释放,此类变量增多,内存压力随之增高。
2.22 遗忘清理的计时器
程序中常使用计时器,即setInterval和setTimeout。
var timeId = setInterval(function(){
// 函数体
},1000)计时器内部逻辑无穷无尽。当定时器囊括的函数逻辑不再需要,而我们又忘记手动清除时,它们将永远占用内存。因此使用定时器时,务必明确清除时机,并使用clearInterval(timeId)手动清除。
2.23 遗忘清理的dom元素引用
var divObj = document.getElementById('mydiv')
// dom删除myDiv
document.body.removeChild(divObj);
console.log(divObj);
// 能console出整个div 说明没有被回收,引用存在
// 移出引用
divObj = null;
console.log(divObj)
// null3.闭包与循环体
闭包与循环体结合,是闭包最经典的考察方式之一。
3.1 这段代码输出啥
看一个大家熟悉的题目,以上6行代码输出什么?
for(var i=0; i<5; i++){
setTimeout(function(){
console.log(i)
},1000)
}
console.log(i)若是刚入门的新手,可能会给出如下答案:
0 1 2 3 4 5
给出此答案的同学,内心通常这样想:for循环输出0-4个i的值,最后一行打印5,setTimeout似曾相识但具体原理印象不深,干脆忽略。
基础较好的同学,若对setTimeout函数用法特性仍有印象,很快会给出“进化版”答案:
5 0 1 2 3 4
这部分同学这样想:for循环逐个输出0-4的值,但setTimeout将输入延迟1s,故最后一行先执行,先输出5,随后过1000ms,0-4逐个输出。
若对JS中的for循环、同步与异步区别、变量作用域、闭包有正确理解,可知正确答案应为:
5 5 5 5 5 5
尝试分析正确答案:seTimeout内函数延迟1000ms后执行,最后一行console先输出,最后一行输出5,故第一个值是5。
for(var i =0;i<5;i++){
// 5<5? 不满足
}
console.log(i) // 5for循环里setTimeout执行了5次,函数延迟1000ms执行。观察该函数,其自身作用域并无i变量,根据作用域链查找规则,要输出i需去上层查找。
setTimeout(function() {
console.log(i);
}, 1000);然而,该函数首次执行也是1000ms后的事。此时它试图向上一层作用域(即全局作用域)查找名为i的变量,此时for循环已执行完毕,i已进入最终状态5。故当1000ms后该函数真正被执行时,引用到的i值已是5。此时,这段代码的作用域状态示意如下:

对应的作用域关系如下:

接下来的连续四次,均会有一个一模一样的setTimeout回调被执行,其输出的也是同一个全局i,故每次输出均为5。
3.2 改造方法
循环五次,每次却输出一个值,此种输出效果显然不佳。若希望让i从0-4依次被输出,我们该如何改造?
方案一:利用setTimeout中第三个参数
开头先复习setTimeout参数用法:
setTimeout(function(arg1,arg2){
console.log(arg1);
console.log(arg2);
},delay,arg1,arg2)- function(必须):调用函数执行的代码块
- delay(可选):函数调用延迟的毫秒值,默认是0,意味着马上执行
- arg1,...arg2(可选):附加参数,当计时器启动时,会作为参数传递给function
请看示例:
setTimeout(function(a,b){
console.log(a); // 1
console.log(b); // 2
},1000,1,2)需注意一点,附加参数仅支持在ie9及以上浏览器,如要兼容,需引入一段MDN提供的兼容旧IE代码。
利用setTimeout的第三个参数,i作为形参传递给setTimeout的j,由于每次传入的参数是从for循环里面取到的值,故会依次输出0~4:
for(var i=0; i<5; i++){
setTimeout(function(j){
console.log(j) // 0 1 2 3 4
},1000,i)
}方案二:使用闭包
使用闭包时,常会用到匿名函数。匿名函数也叫一次性函数,在函数定义时执行,且只执行一次。我们在setTimeout外面套一个匿名函数,利用匿名函数的实参来缓存每一个循环的i值。
for(var i= 0; i<5; i++){
(function(j){
setTimeout(function(){
console.log(j)
},1000)
})(i)
}当输出j时,引用的是外部函数传递的变量i,这个i是根据循环来的,执行setTimeout时已经确定了里面i的值,进而确定了j的值。
方案三:使用let
for(let i= 0; i<5; i++){
setTimeout(function(){
console.log(i)
},1000)
}for循环每次循环产生一个新的块级作用域,每个块级作用域的变量是不同的。函数输出的是自己的上一级(循环产生的块级作用域)下i的值。
4.总结
- 作用:模拟私有变量、柯里化、偏函数、防抖、节流、实现缓存。
- 模拟私有变量:将私有变量放在外在的立即执行函数中,并通过立即执行U这个函数,创造一个闭包环境(私有变量:只允许函数内部,或对象方法访问的变量)。
- 柯里化:把接受n个参数的一个函数转化成只接受一个参数n个函数互相嵌套的函数过程,目标是把函数拆解为精准的n部分,也就是将fn(a,b,c)转化成fn(a)(b)(c)的过程。
- 偏函数:固定函数中的某一个或几个参数,然后返回一个新的函数,不强调但函数。
- 防抖:只执行最后一次。
- 节流:隔一段时间执行一次。
- 缓存变量:计时器打印问题。
- 闭包造成内存泄露问题是误传,误传来源于IE浏览器bug,可以放心大胆使用。
以上就是一文详解JavaScript闭包典型应用的详细内容,更多关于JavaScript闭包典型应用的资料请关注本站其它相关文章!
JS语法错误会阻止加载吗?区分SyntaxError与资源加载失败
2026-11-05 16:08:46
JavaScript常见SyntaxError语法错误排查与修复方法
2026-11-05 13:58:44
JS中slice、substring和substr截取字符串的区别
2026-11-05 13:43:47
JS 转 TS 手动迁移最佳实践:从配置 tsconfig 到消除 any 类型
2026-11-05 11:53:32
为什么JS数组push后得到的是数字而不是数组?
2026-11-05 09:59:18
