闭包实战大全:从防抖节流到私有变量,解锁JS高级技巧 🚀
"闭包是JavaScript中的魔法,它让函数拥有了记忆。" ------ 某个前端魔法师
引言:闭包是什么?为什么重要?
闭包是JavaScript中最强大也最常被误解 的概念之一。简单说,闭包就是函数与其词法作用域的组合 ,它让函数可以"记住"并访问其创建时的环境,即使该函数在其他地方执行。想更深入了解闭包的底层执行机制可以看看这篇文章揭秘JavaScript执行机制:作用域链、词法作用域与闭包揭秘JavaScript执行机制:作用域链、词法作用域与闭包 - 掘金
今天,我将带大家深入探索闭包的各种实际应用场景,结合代码示例,让你彻底掌握这个JavaScript核心概念!

一、防抖(Debounce)和节流(Throttle):控制事件触发频率和保证定期执行
防抖是闭包的经典应用场景,它确保事件处理函数在连续触发时只执行一次。
html
<!DOCTYPE html>
<html lang="en">
<body>
<h2>没有防抖</h2>
<input type="text" id="inputA">
<h2>进行了防抖</h2>
<input type="text" id="inputB">
<script>
function debounce(fn, delay) {
return function(args) {
clearTimeout(fn.id)
fn.id = setTimeout(() => fn(args), delay)
}
}
const inputA = document.getElementById('inputA')
const inputB = document.getElementById('inputB')
function ajax(content) {
console.log('ajax request: ' + content)
}
// 普通输入 - 每次按键都触发
inputA.addEventListener('keyup', e => ajax(e.target.value))
// 防抖输入 - 停止输入500ms后才触发
const debounceAjax = debounce(ajax, 500)
inputB.addEventListener('keyup', e => debounceAjax(e.target.value))
</script>
</body>
</html>
节流确保函数在指定时间间隔内最多执行一次(在一段时间内,连续触发中必会执行一次),特别适合scroll等高频事件。
javascript
function throttle(fn, delay) {
let last, deferTimer
return function(...args) {
const that = this
const now = +new Date()
if (last && now < last + delay) {
clearTimeout(deferTimer)
deferTimer = setTimeout(() => {
last = now
fn.apply(that, args)
}, delay)
} else {
last = now
fn.apply(that, args)
}
}
}
// 使用示例
const throttleAjax = throttle(ajax, 500)
window.addEventListener('scroll', () => throttleAjax('scroll event'))

-
闭包的体现:
debounce
内部返回的匿名函数引用了外部的fn
、delay
和fn.id
变量,形成闭包。- 闭包让这些变量始终存活,即使
debounce
函数已执行完毕。
-
为什么用闭包:
- 保存独立状态 :每个防抖函数实例(如
debounceAjax
)都有自己的定时器 ID 和延迟时间,互不干扰。 - 封装私有变量 :
fn.id
和delay
无法被外部访问,保证逻辑安全。 - 参数传递 :闭包记住了
fn
是ajax
,并能传递e.target.value
。
- 保存独立状态 :每个防抖函数实例(如
三、封装私有变量:实现真正封装
闭包是实现私有变量的最佳方式,让我们创建真正封装的类:
javascript
function Book(title, author, year) {
// 私有变量
let _title = title
let _author = author
let _year = year
// 私有方法
function getFullTitle() {
return `${_title} by ${_author}`
}
// 公有API
this.getTitle = () => _title
this.getFullInfo = () => `${getFullTitle()}, published in ${_year}`
this.updateYear = newYear => {
if (typeof newYear === 'number' && newYear > 0) _year = newYear
else console.error('Invalid year')
}
}
const book = new Book('JS高级编程', '尼古拉斯', 2023)
console.log(book._title) // undefined - 真正私有!
console.log(book.getTitle()) // "JS高级编程"
book.updateYear(2024)
闭包如何实现封装?
- 构造函数中的局部变量是私有的
- 公共方法作为闭包捕获这些私有变量
- 外部无法直接访问私有变量,只能通过公共API
闭包的意义
- 数据隐藏与封装 :
_title
,_author
, 和_year
变量以及getFullTitle
方法被定义在构造函数Book
内部,这意味着它们对外部是不可访问的(即真正的私有)。这提供了一种机制来隐藏对象的内部状态,防止外部代码随意修改这些值。 - 保护对象的状态 :通过只暴露必要的公有方法(如
getTitle
,getFullInfo
,updateYear
),可以控制如何读取或修改内部状态。例如,updateYear
方法不仅允许更新年份,还包含了验证逻辑以确保新值的有效性。 - 保持函数上下文 :内部函数(如
getFullTitle
)能够访问构造函数内的局部变量(即使构造函数已经执行完毕),这是因为它们形成了闭包。这种特性使得这些函数能够在后续调用时依然能访问到创建时的作用域中的变量。
四、解决this丢失问题:闭包与上下文绑定
闭包结合箭头函数或bind方法可以完美解决JavaScript中的this
指向问题:
html
<button id="myButton">Click Me</button>
<script>
const obj = {
message: "Hello from object",
init() {
const button = document.getElementById('myButton')
// 方案1:使用箭头函数(闭包捕获this)
button.addEventListener('click', () => {
console.log(this.message) // 正确!
})
// 方案2:使用闭包保存this
const that = this
button.addEventListener('click', function() {
console.log(that.message) // 正确!
})
// 方案3:使用bind
button.addEventListener('click', function() {
console.log(this.message) // 正确!
}.bind(this))
}
}
obj.init()
</script>
一、方案 1:箭头函数(闭包捕获 this)
javascript
button.addEventListener('click', () => {
console.log(this.message) // 正确!
})
闭包体现:
产生闭包的主体 :箭头函数 () => { console.log(this.message) }
-
箭头函数没有自己的
this
,它捕获的this
来自定义时的上下文(即obj.init()
中的this
,指向obj
)。 -
即使事件处理函数在点击时才执行(此时
this
通常指向 DOM 元素),闭包仍保留了定义时的this
值。
关键点:
- 箭头函数通过闭包隐式捕获了
this
变量。 - 无论何时执行,
this
始终指向obj
。
为什么产生闭包 :
当箭头函数在 obj.init()
方法内部被定义时,它会捕获当前词法作用域中的 this
变量 (此时 this
指向 obj
)。即使该箭头函数作为事件处理函数在点击时才执行(此时事件处理函数的执行上下文通常会指向触发事件的 DOM 元素),它仍能通过闭包访问并使用定义时保存的 this
值(即 obj
)
与普通函数不同之处 是箭头函数的
this
绑定由定义时的上下文决定,而非调用时的上下文。
二、方案 2:变量保存 this(显式闭包)
javascript
const that = this
button.addEventListener('click', function() {
console.log(that.message) // 正确!
})
闭包体现:
- 将
this
保存到变量that
中,that
被闭包捕获。 - 事件处理函数(普通函数)的
this
指向 DOM 元素,但通过闭包访问的that
始终指向obj
。
关键点:
- 显式创建变量
that
,并通过闭包保持其引用。 - 普通函数的
this
被绕过,直接使用闭包中的that
。
普通函数的
this
指向由调用方式 决定。当它作为事件处理函数被浏览器调用时(如点击按钮时),浏览器会默认将this
绑定到触发事件的 DOM 元素(此处为button
)。因此我们要通过闭包访问外部函数的this
三、方案 3:使用 bind(函数绑定 + 闭包)
javascript
button.addEventListener('click', function() {
console.log(this.message) // 正确!
}.bind(this))
闭包体现:
bind(this)
创建了一个新函数,其中this
被永久绑定为obj.init()
中的this
。- 闭包捕获的不是
this
本身,而是通过bind
绑定的上下文。
关键点:
bind
返回的函数通过闭包记住了绑定的上下文。- 事件处理函数执行时,
this
已经被bind
固定为obj
。
四、call,apply绑定与bind的区别
call,apply语法:
-
fn.call(thisArg, arg1, arg2, ...)
-
fn.apply(thisArg, [arg1, arg2, ...])
核心逻辑 :
当调用fn.call(obj)
时,函数fn
会立即执行,且执行期间this
指向obj
;执行结束后,fn
的this
指向不会被永久改变(下次正常调用时this
仍遵循默认规则)。
不同处:
call,apply绑定不涉及闭包 , 闭包的核心是 函数在定义时捕获外部变量,并在后续执行时(即使脱离原作用域)仍能访问这些变量(本质是保留对原作用域的引用)。
而call
/apply
的逻辑完全不同:
- 它们不创建新函数,只是对原函数进行 "即时调用";
- 它们不保留任何变量引用 :绑定的
this
仅在函数执行的那一刻有效,执行结束后与原作用域、变量均无关联; - 函数执行时的
this
是通过临时绑定机制(如上述的 "临时属性")实现的,而非通过闭包捕获外部变量。
因此,call
/apply
的工作过程中,没有 "函数记住外部作用域变量" 的行为,自然不涉及闭包。
方法 | 核心行为 | 是否创建新函数 | 是否依赖闭包 | 关键区别 |
---|---|---|---|---|
call /apply |
立即执行函数,临时绑定this |
否 | 否 | 无状态,仅影响单次执行 |
bind |
创建新函数,永久绑定this |
是 | 是 | 有状态,通过闭包保留绑定上下文 |
简单说:call
/apply
是 "一次性临时借用上下文",bind
是 "创建一个永久记住上下文的新函数"------ 后者需要闭包,前者不需要。
总结:三种方案的闭包对比
方案 | 闭包捕获对象 | 核心机制 | 特点 |
---|---|---|---|
箭头函数 | 直接捕获this |
箭头函数无自己的this |
语法简洁,隐式保留上下文 |
变量保存 | 捕获变量that |
显式保存this 到变量 |
兼容性好,适用于所有函数类型 |
bind 方法 | 捕获绑定的上下文 | 通过bind 创建新函数 |
语义明确,清晰表达绑定意图 |
五、记忆函数:提升性能的利器
闭包可以实现函数结果的缓存,避免重复计算:
javascript
function memoize(fn) {
const cache = new Map()
return function(...args) {
const key = JSON.stringify(args)
if (cache.has(key)) {
console.log('从缓存中获取结果')
return cache.get(key)
}
const result = fn.apply(this, args)
cache.set(key, result)
return result
}
}
// 使用示例
const expensiveCalc = n => {
console.log('执行复杂计算...')
return n * n
}
const memoizedCalc = memoize(expensiveCalc)
console.log(memoizedCalc(5)) // 执行计算
console.log(memoizedCalc(5)) // 从缓存获取
六、模块模式:使用IIFE创建私有空间
立即执行函数表达式(IIFE)结合闭包创建模块:
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>立即执行函数 IIFE</title>
</head>
<body>
<script>
const Counter = (function(){
let count = 0;//私有变量 自由变量(不会被销毁,处于闭包当中)
function increment(){
return ++count;
}
function reset(){
count = 0;
}
return function(){
return{
getCount:function(){
return count;
},
increment:function(){
return increment();
},
reset:function(){
return reset();
}
}
}
})();
const counter1 = Counter()
const counter2 = Counter()
console.log(counter1.getCount())//0
counter1.increment()//1
console.log(counter2.getCount())//1
//console.log(counter2.increment())//1
</script>
</body>
</html>
可以这样理解,具体分析如下:
- Counter 的类型 :
Counter 是一个函数 。
因为 IIFE 执行后返回的是一个function(){ ... }
(外层函数返回的内部函数),所以const Counter = ...
实际上是将这个函数赋值给了 Counter。 - counter1、counter2 的类型 :
是对象 。
当调用Counter()
时,执行了上述返回的函数,该函数返回一个包含getCount
、increment
、reset
方法的对象,因此 counter1 和 counter2 都是这样的对象实例。 - 闭包共享的原因 :
所有通过Counter()
创建的对象(counter1、counter2),其内部方法(getCount
等)共享同一个闭包。
因为它们的定义都嵌套在最外层的 IIFE 中,共同访问 IIFE 作用域内的私有变量count
。因此,无论创建多少个对象实例,操作的都是同一个count
,会相互影响。
简单说:Counter 是 "造对象的函数",counter1/counter2 是 "被造出的对象",但所有对象共享同一个闭包作用域里的 count
。
闭包面试通关秘籍
当面试官问及闭包时,你可以这样回答:
-
基本概念:"闭包是函数与其词法环境的组合,即使函数在原始作用域外执行,也能访问该作用域"
-
核心价值:
- 创建私有变量和方法
- 保持状态(如计数器)
- 实现高阶函数(防抖、节流等)
- 模块化开发
-
实际应用:
graph TD A[闭包应用] --> B[防抖节流] A --> C[私有变量封装] A --> D[函数柯里化] A --> E[记忆函数] A --> F[模块模式] A --> G[解决this问题] -
注意事项:
- 避免内存泄漏(不再使用的闭包及时释放)
- 不要过度使用(可能增加内存消耗)
- 理解作用域链
结语:闭包的力量
闭包是JavaScript中真正强大的特性之一,它不仅仅是面试题中的常客,更是实际开发中不可或缺的工具。掌握闭包,你就能:
✅ 写出更优雅、高效的代码
✅ 解决复杂的业务问题
✅ 设计更好的软件架构
✅ 在面试中脱颖而出
闭包就像JavaScript的"超能力",现在你已经拥有了它,去创造令人惊叹的代码吧!💪

"任何可以用JavaScript编写的应用,最终都将用JavaScript编写。" ------ Atwood定律