闭包是 JavaScript 的核心概念之一,它允许函数访问并记住其创建时的作用域环境,即使该函数在其原始作用域外执行,这种特性赋予了闭包强大的能力,使其在多种场景中发挥着关键作用。
今天我们介绍的是应用场景中的封装私有变量、函数防抖和函数节流,下面,我将通过结合代码示例对此进行详细解析。
一、什么是闭包?
闭包是JavaScript中一个重要的概念,它指的是一个函数与其周围状态(词法环境)的组合。简单来说,闭包让函数可以"记住"并访问它创建时的作用域,即使这个函数在其原始作用域外执行。
闭包的作用:
- 记忆函数:闭包能保存函数创建时的变量状态,使嵌套函数能访问外部变量
- 数据封装:闭包支持创建私有变量和函数
- 状态保持:闭包能延长局部变量的生命周期
二、封装私有变量:解决数据安全问题
1. 封装之前:人人都可修改,数据暴露无遗
先来看一段简单的计数器实现:
let counter = {
count: 0,
increment() {
this.count++;
},
decrement() {
this.count--;
}
};
// 外部可以直接修改数据
counter.count = 1000;
console.log("当前计数:", counter.count); // 输出 1000
上面这段代码虽然实现了基本的计数功能,但是,其中存在严重的 数据安全性问题,如:
- 数据暴露无遗:
count是公开属性,外部可以随意修改为任何值(如负数、字符串等)。 - 缺乏校验机制:
increment和decrement方法没有做参数检查或边界控制。 - 业务规则被破坏:比如库存数量、购物车商品数等关键数据可能因此被篡改,导致系统异常。
2. 封装之后:闭包保护数据,真正达成私有
我们通过函数作用域 + 闭包的方式,将 count 变成真正私有的变量,只能通过指定的方法访问和修改。
function CreateCounter(num) {
// 私有变量,外界无法直接访问
let count = num;
return {
num, // 公共属性,可读不可写
increment: () => {
count++;
},
decrement: () => {
if (count > 0) {
count--;
}
},
getCount: () => {
console.log("count 被访问了!!!");
return count;
}
};
}
运行结果:
const counter = CreateCounter(0);
counter.increment();
counter.increment();
counter.decrement();
console.log(counter.getCount()); // 输出 1
console.log(counter.num); // 输出 0(只读)
console.log(counter.count); // 输出 undefined(无法访问私有变量)
从上面的案例可以看出,闭包是 JavaScript 中非常强大的特性,它能够让函数能够“记住”并访问其创建时所处的作用域,即使该函数在其外部执行。
闭包的作用:
(1)实现真正的“私有变量”
count定义在CreateCounter函数内部,外部无法直接访问。- 唯一能操作
count的方式是通过返回对象中的方法(如increment、decrement、getCount),这就形成了数据的封装和访问控制。
(2)延长变量生命周期
- 即使
CreateCounter函数已经执行完毕,count这个局部变量仍然不会被垃圾回收,因为它被闭包函数引用。 - 换句话说,这些方法“记住”了它们出生时的环境,并持续保留着对
count的访问权限。
(3)对外提供受控接口
- 用户不能直接修改
count,只能通过调用方法间接操作。 - 我们可以在方法内部加入校验逻辑(例如防止负数),从而确保数据始终合法。
三、函数防抖:解决搜索建议的"疯狂请求"问题
1.防抖之前:搜索过度热情,请求积极响应
在实现实时搜索时,传统方法一般会导致过度请求,如以下代码,在输入一个字符串时,每按下一个字母它就会发送1次请求,而这也导致造成了大量的 资源浪费、响应顺序错乱和页面卡顿的现象:
let inputA = document.getElementById('inputA')
function ajax(cotent) {
console.log(cotent)
}
inputA.addEventListener('keyup', function (event) {
ajax(event.target.value)
})
2. 防抖之后:搜索固定响应,对你定时搭理
通过使用闭包,我们就能使其在单位时间之内只执行一次,而其他时候不执行,大大优化了资源使用:
let inputB = document.getElementById('inputB')
function ajax(cotent) {
console.log(cotent)
}
function debounce(fn, delay) {
return function (args) {
if (fn.id) {
clearTimeout(fn.id)
}
clearTimeout(fn.id)
fn.id = setTimeout(function () {
fn(args)
}, delay)
}
}
let dobounceAjax = debounce(ajax, 1000)
inputB.addEventListener('keyup', function (event) {
dobounceAjax(event.target.value)
}
)
可以看到,在优化后,用户在输入框中快速连续输入时,代码不会每次都立刻执行 ajax 请求,而是在用户停止输入一秒后才真正触发一次请求。
闭包的作用
(1)保持定时器状态:通过闭包保存定时器ID (fn.id),使得每次调用函数时可以取消前一个定时器并设置新的定时器,确保只有在用户停止输入超过设定延迟(如1秒)后,才会执行目标函数。
(2)提供封装性:保护内部使用的变量不被外部访问或修改,减少全局命名空间污染。
简而言之,闭包帮助实现了防抖功能,优化了资源使用和性能。
四、函数节流:解决滚动加载或窗口调整的“性能过载”问题
1. 节流之前:事件频频触发,代码跑到累死
在网页中,有些事件会被高频触发,比如:窗口大小调整(resize)、页面滚动(scroll)和鼠标移动(mousemove)事件。
例如,下面这段代码会在每次窗口大小变化时都执行一次操作:
window.addEventListener('resize', () => {
console.log("窗口你变了!");
});
如果用户快速拖动窗口大小,这个回调会频繁执行,这就可能会造成大量的 DOM 操作、页面布局重新计算(reflow)和性能下降甚至卡顿,导致 浪费资源、效率低下。
2. 节流之后:任你如何催促,慢跑连带散步
通过下面这段代码,我们可以通过 函数节流(Throttle) 的方式,让某个函数 每隔一段时间只执行一次,即使它被频繁调用,我们也是每隔单位时间只执行一次,其他的则会被延迟执行。
function throttle(fn, delay) {
let last = 0;
return function (...args) {
const now = Date.now();
if (now - last >= delay) {
fn.apply(this, args);
last = now;
}
};
}
// 使用节流包装一个窗口大小监听函数
const smartResize = throttle((width) => {
console.log("当前窗口宽度:", width);
}, 500);
// 绑定 resize 事件
window.addEventListener('resize', () => {
smartResize(window.innerWidth);
});
闭包的作用:
(1)throttle 函数的作用
-
接收两个参数:
fn:需要节流的目标函数(比如打印窗口宽度)delay:两次执行之间的最小间隔时间(单位是毫秒)
-
返回一个新的函数,这个函数才是真正被调用的“节流版”函数。
(2)关键变量:last 和它的命运
let last = 0;
- 这个变量记录上一次函数执行的时间戳。
- 它定义在
throttle函数内部,按理说在函数执行完后应该被销毁。 - 但由于返回的函数引用了它,所以它不会被垃圾回收,这就是闭包的力量!
(3)返回的函数
return function (...args) {
const now = Date.now();
if (now - last >= delay) {
fn.apply(this, args);
last = now;
}
};
- 每次调用都会检查当前时间和上次执行时间的差值。
- 如果超过设定的
delay,才允许执行目标函数,并更新last。 - 否则就跳过本次调用,避免重复执行。
五、 总结一句话
通过 函数节流 + 闭包 的方式,我们可以有效控制高频事件的执行频率,避免不必要的性能开销。
其中,闭包让函数能够记住和操作之前的状态(如上次执行时间),是实现节流机制的核心所在。