变量的作用域
要理解闭包,必须要理解js的特殊的变量作用域。变量的作用域分为两种:全局变量和局部变量。
js语言的特殊之处在于,函数内部可以直接读取全局变量
var n=999;
function f1(){
alert(n);
}
f1(); // 999
另一方面,在函数外部无法获取函数内部的局部变量
function f1(){
var n=999;
}
alert(n); // error
PS: 有个地方需要特别注意的,在函数内部声明变量的时候,一定要使用 var 变量,如果不用,则实际上是声明了一个全局变量
function f1(){
n=999;
}
f1();
alert(n); // 999
如何从外部获取局部变量
由于种种原因 有时候需要得到函数内部的局部变量,但是前面提到过,正常情况下是办不到的,只有通过变通的方法才能实现。那就是在函数的内部,再定义一个函数
function f1(){
var n = 999;
function f2(){
alert(n) // 999
}
}
在上面的代码中,函数f2就被定义在函数f1的内部,这是f1内部的所有的局部变量,对f2就是可见的,反之则不可以。这就是js特有的链式作用域结构,子对象会一级一级的向上寻找所有父对象的变量。
function f1(){
var n = 999;
function f2(){
console.log(n)
}
return f2
}
var result = f1()
result() // 999
所以因此可见 f2 函数就是闭包
闭包的概念
简单说,闭包就是能够读取其他函数内部变量的函数。在js中,只有函数内部的子函数才能读取读取局部变量,因此可以把闭包理解成定义在一个函数内部的函数。所以闭包就是函数内部与函数外部沟通起来的桥梁。
闭包的用途
闭包可以用在很多地方,最大的用途有两个:1、可以读取函数内部的变量 2、让变量始终保存在内存中 该如何理解呢 下面举例说明
function f1(){
var n=999;
nAdd=function(){n+=1}
function f2(){
alert(n);
}
return f2;
}
var result=f1();
result(); // 999
nAdd();
result(); // 1000
在这段代码中,result实际上就是闭包f2函数。它一共运行了两次,第一次的值是999,第二次的值是1000。这证明了,函数f1中的局部变量n一直保存在内存中,并没有在f1调用后被自动清除。
为什么会这样呢?原因就在于f1是f2的父函数,而f2被赋给了一个全局变量,这导致f2始终在内存中,而f2的存在依赖于f1,因此f1也始终在内存中,不会在调用结束后,被垃圾回收机制(garbage collection)回收。
这段代码中另一个值得注意的地方,就是"nAdd=function(){n+=1}"这一行,首先在nAdd前面没有使用var关键字,因此nAdd是一个全局变量,而不是局部变量。其次,nAdd的值是一个匿名函数(anonymous function),而这个匿名函数本身也是一个闭包,所以nAdd相当于是一个setter,可以在函数外部对函数内部的局部变量进行操作。
function outer() {
let count = 0;
return function inner() {
count++;
console.log(count);
};
}
const fn = outer();
fn(); // 1
fn(); // 2
执行过程分析
| 步骤 | 描述 |
|---|---|
| ① | 执行 outer(),创建局部变量 count = 0,返回 inner 函数 |
| ② | 由于 inner 引用了 count,所以 count 不会被 GC 回收 |
| ③ | 执行 fn()(即 inner)时访问闭包中的 count,执行 count++
|
| ④ | 每次调用 fn(),闭包都会在原有的 count 状态基础上累加 |
| ⑤ | 若想打印旧值,则用 console.log(count++)
|
PS:补充说明一点,闭包能够保持变量的状态的本质是外部函数执行完毕后,其局部变量应该被销毁,但只要还有内部函数(闭包)还有引用,js引擎就不会释放这块内存。
闭包使用的注意点
1、由于闭包会使得函数中的变量都保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页性能问题。解决方法是,在退出函数之前,将不使用的局部变量全部删除。
举个例子说明如何退出函数前将不用的局部变量删除
function createUser() {
let name = "张三";
let age = 25;
let address = "北京";
return {
getName() {
return name;
},
destroy() {
// 不再需要时,手动清理引用
name = null;
age = null;
address = null;
}
};
}
const user = createUser();
console.log(user.getName()); // 张三
// 用完后
user.destroy(); // 手动释放闭包中不再使用的变量
2、闭包就像给函数加了“记忆力”,能在外部访问并修改它内部的变量,但这也可能破坏原本封装好的数据,所以要像保护对象的私有属性一样谨慎使用。
举例:
function createUser() {
let name = '张三';
return {
setName(newName) {
name = newName;
},
getName() {
return name;
},
// ❌ 一个危险的方法
resetAll() {
name = null; // 直接清空
}
};
}
const user = createUser();
user.setName('李四');
console.log(user.getName()); // 李四
user.resetAll(); // ⚠️ 破坏性操作
console.log(user.getName()); // null ❌
这样 resetAll() 就破坏了原来“name 不该随意清空”的规则。
闭包的实际应用场景
1、防抖和节流
应用特性:闭包能保存状态(保存上一次定时器或时间戳)
防抖:在事件被触发 n 秒后才执行回调,如果在这 n 秒内事件又被触发,则重新计时。
典型场景:输入框联想搜索、窗口 resize、按钮防重复点击。
核心目的:减少频繁调用函数的次数。
节流:规定一个固定时间间隔,在这段时间内无论事件触发多少次,回调函数只执行一次。
典型场景:滚动监听、窗口 resize、鼠标移动事件。
核心目的:控制函数执行频率
2、私有变量/数据封装
应用特性:闭包能访问外部函数的变量,但外部无法直接访问它,实现“私有变量 + 公共方法”。
function createCounter() {
let count = 0; // 私有变量
return {
increment() { count++; },
getCount() { return count; }
};
}
const counter = createCounter();
counter.increment();
console.log(counter.getCount()); // 1
count 只对闭包内部方法可见,外部无法直接访问。
3、模块化开发
应用特性:闭包延长变量生命周期,隐藏内部实现,只暴露公共方法。
const CounterModule = (function() {
let count = 0; // 私有变量
function add() { count++; }
function get() { return count; }
return { add, get }; // 暴露公共方法
})();
CounterModule.add();
console.log(CounterModule.get()); // 1
count 对外不可访问,只能通过 add() 和 get() 操作。
补充防抖和节流的实现
防抖的实现代码
function debounce(fn, delay) {
let timer = null; // A:记住上一次的定时器 id(闭包状态)
return function (...args) { // B:返回的函数会在外部被频繁调用
clearTimeout(timer); // C:每次调用先把上一次的定时器取消掉
timer = setTimeout(() => { // D:建立一个新的定时器:delay 后执行真正的 fn
fn.apply(this, args);
}, delay);
};
}
每次触发都重置一个延时任务,只有最后一次触发后静止达到 delay,真正的 fn 才会被执行。
举例说明:
假设 delay = 5s:
t=0s:第一次触发 → 设闹钟(将在 t=5s 时执行 fn)
t=4s:第二次触发(在第一次的 5s 未到之前) → 先把闹钟关掉(取消第一次),再重新设新的闹钟(将在 t=9s 执行)
t=8s:又触发 → 关掉 t=9s 的闹钟,再设新的(将在 t=13s 执行)
如果从 t=8s 后 没人再触发,那么在 t=13s fn 才会真正执行。
一句话:只有最后一次触发后的完整延时才会触发 fn。
使用示例:
function handleClick() {
console.log("按钮被点击了");
}
const debouncedClick = debounce(handleClick, 2000);
节流的代码实现
function throttle(fn,delay){
let lastTime = 0;
return function(..args){
const now = Date.now();
if(now - lastTime >= delay){
fn.apply(this,args);
lastTime = now
}
}
}
循环陷阱
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 1000);
}
// 输出:3 3 3
原因是因为var是函数作用域不是块级作用域,所以循环中的 i 全局只有一个变量,每次循环都会修改这个同一个变量。循环结束时,i = 3(因为循环条件 i < 3 不成立)
时间 i 值 发生的事情
t0 0 第一次循环:执行 setTimeout,回调捕获 i 的引用
t0 1 第二次循环:执行 setTimeout,回调捕获 i 的引用
t0 2 第三次循环:执行 setTimeout,回调捕获 i 的引用
t0 3 循环结束,i = 3
t0+1000ms 3 第一次 setTimeout 回调执行,打印 i → 3
t0+1000ms 3 第二次 setTimeout 回调执行,打印 i → 3
t0+1000ms 3 第三次 setTimeout 回调执行,打印 i → 3