在 JavaScript 的学习过程中,闭包(Closure)是一个让人既困惑又着迷的概念。很多初学者觉得它难以捉mō,但在项目中,你已经在不知不觉中使用它了。
闭包就是函数能够记住并访问它定义时的作用域,即使这个函数在定义它的作用域之外执行。这就好比一个背包,函数离开原处时,还背着原作用域里的变量。
闭包的形成条件
闭包的形成需要两个条件:
-
函数内部定义了另一个函数(内嵌函数)
-
内嵌函数引用了外部函数的变量
来看一个最基本的例子:
// 代码号学习编程:基础闭包示例
function createGreeting(name) {
// 外部函数的局部变量
let message = "欢迎,";
// 内部函数使用了外部函数的变量
function greet() {
return message + name + "!";
}
return greet; // 返回内部函数本身
}
const greetRohit = createGreeting("Rohit");
console.log(greetRohit()); // 输出: 欢迎,Rohit!
代码执行后,createGreeting 函数已经运行完毕,按理说它的内部变量 message 和参数 name 应该被销毁了。但当我们调用 greetRohit() 时,依然能访问到 "欢迎," 和 "Rohit"。这就是闭包的作用——greet 函数把它定义时的环境“打包”带走了。
词法作用域:闭包的基石
理解闭包,要明白词法作用域(Lexical Scoping)。它的核心规则是:函数的作用域由函数定义的位置决定,而不是由调用位置决定。
function outer() {
const courseName = "JavaScript 高级教程";
function inner() {
// inner 在这里定义,所以它能访问 outer 里的变量
console.log("课程名称: " + courseName);
}
inner(); // 调用位置在 outer 内部
}
outer(); // 输出: 课程名称: JavaScript 高级教程
即使把 inner 函数拿到外面去调用,它依然记得 courseName:
function outer() {
const courseName = "闭包专题讲解";
function inner() {
console.log("课程名称: " + courseName);
}
return inner; // 把 inner 返回出去
}
const innerFunc = outer();
innerFunc(); // 调用位置在全局,但依然能访问 courseName
// 输出: 课程名称: 闭包专题讲解
这就是词法作用域的体现——作用域链在函数定义时就确定了,闭包则负责在函数执行时维持这条链。
闭包的实用场景
闭包不是理论概念,它在日常开发中有很多实际用途。
1. 私有变量(数据封装)
在项目中,有些数据不希望被随意修改,这时可以利用闭包创建私有变量。
// 代码号学习编程:创建计数器模块
function createCounter() {
// count 是私有变量,外部无法直接访问
let count = 0;
return {
increment: function() {
count++;
console.log("当前计数: " + count);
},
decrement: function() {
count--;
console.log("当前计数: " + count);
},
getCount: function() {
return count;
}
};
}
const counter = createCounter();
counter.increment(); // 输出: 当前计数: 1
counter.increment(); // 输出: 当前计数: 2
console.log(counter.count); // 输出: undefined (无法直接访问)
console.log(counter.getCount()); // 输出: 2 (通过方法获取)
这种方式比使用全局变量安全得多。团队协作时,如果有人不小心写了 counter.count = 100,也不会影响内部真正的 count 值。
2. 函数柯里化(Currying)
柯里化是把接受多个参数的函数,转换成一系列接受单个参数的函数。闭包在这里的作用是“记住”之前传入的参数。
// 普通加法函数
function add(a, b) {
return a + b;
}
// 使用闭包实现柯里化
function curriedAdd(a) {
return function(b) {
return a + b;
};
}
const addFive = curriedAdd(5); // 固定第一个参数为 5
console.log(addFive(3)); // 输出: 8
console.log(addFive(10)); // 输出: 15
const addTen = curriedAdd(10);
console.log(addTen(20)); // 输出: 30
这种模式在配置函数、延迟计算等场景中很有用,能提高代码的复用性。
3. 异步操作中的状态保持
在处理 setTimeout、事件监听或 AJAX 请求时,闭包能帮我们记住异步操作发生时的状态。
function delayedMessage(message, delay) {
setTimeout(function() {
// 这个内部函数形成了闭包,记住了 message 参数
console.log("延迟消息: " + message);
}, delay);
}
delayedMessage("你好,2026年!", 2000); // 2秒后输出
delayedMessage("闭包依然有效", 3000); // 3秒后输出
如果不用闭包,等到 setTimeout 的回调执行时,变量 message 早就被他值覆盖了。
4. 使用 IIFE 创建独立作用域
立即执行函数表达式(IIFE)结合闭包,可以创建独立的作用域,避免变量污染全局。
// 创建一个模块,内部数据对外不可见
const userModule = (function() {
// 私有变量
let username = "游客";
let loginCount = 0;
// 返回公共 API
return {
setUser: function(name) {
username = name;
loginCount++;
},
getUserInfo: function() {
return `用户: ${username}, 登录次数: ${loginCount}`;
}
};
})();
userModule.setUser("Alan");
console.log(userModule.getUserInfo()); // 输出: 用户: Alan, 登录次数: 1
console.log(userModule.username); // 输出: undefined (无法访问私有变量)
这种方式在封装插件、工具库时很常见,可以只暴露需要的方法,隐藏内部实现细节。
闭包和 this 的微妙关系
闭包内部使用 this 时,会遇到意料之外的情况,因为 this 的指向取决于函数的调用方式,而不是定义位置。
function Student(name) {
this.name = name;
// 方法1: 使用普通函数,this 会丢失
setTimeout(function() {
console.log("普通函数中的 this.name:", this.name);
}, 1000);
// 方法2: 使用箭头函数,继承外部的 this
setTimeout(() => {
console.log("箭头函数中的 this.name:", this.name);
}, 1000);
// 方法3: 使用 bind 绑定 this
setTimeout(function() {
console.log("bind 后的 this.name:", this.name);
}.bind(this), 1000);
}
const student = new Student("Rohit");
// 1秒后输出:
// 普通函数中的 this.name: undefined
// 箭头函数中的 this.name: Rohit
// bind 后的 this.name: Rohit
从代码运行结果可以看出,普通函数里的 this 不再指向 student 实例。解决这个问题,可以用箭头函数(它不绑定自己的 this),或者在外部把 this 保存到一个变量里(比如 const self = this),然后用闭包去访问那个变量。
闭包的注意事项
内存管理
闭包会让外部函数的变量一直活在内存里,不会被垃圾回收机制清理。如果滥用闭包,尤是在循环中创建大量闭包,会占用较多内存。
function processItems(items) {
// 每个 item 的处理函数都是一个闭包
let handlers = [];
for (let i = 0; i < items.length; i++) {
// 注意:这里用 let,如果用 var 会有不同结果
handlers.push(function() {
console.log("处理第 " + i + " 项: " + items[i]);
});
}
return handlers;
}
let handlers = processItems(["A", "B", "C"]);
handlers[0](); // 输出: 处理第 0 项: A
handlers[1](); // 输出: 处理第 1 项: B
在这个例子中,每个处理函数都形成了一个闭包,记住了对应的 i 和 items。如果 items 是一个很大的数组,这些闭包会一直持有对它的引用,导致内存无法释放。用完闭包后,如果不再需要,可以手动解除引用(如 handlers = null),帮助垃圾回收。
调试复杂度
闭包增加了作用域链的长度,调试时不容易看清变量来自哪里。在 Chrome 开发者工具的 Sources 面板里,可以通过查看 Scope 区域来观察闭包中保存的变量。
本节课程知识要点
-
闭包的定义:函数与词法环境的组合,使得内层函数能访问外层函数的变量,即使外层函数已执行完毕。
-
形成条件:函数嵌套 + 内层函数引用外层变量。
-
词法作用域:作用域由函数定义位置决定,闭包依赖此规则维持变量访问。
-
核心用途:
-
数据封装:创建私有变量,实现模块化
-
状态保持:让异步操作记住当时的上下文
-
函数柯里化:固定部分参数,生成更具体的函数
-
事件处理:在回调中访问组件实例的数据
-
-
注意事项:
-
闭包造成内存占用,用后及时清理无用引用
-
闭包内
this的指向需要留意,可用箭头函数或bind解决 -
适度使用,避免作用域嵌套过深,影响代码可读性
-
闭包是 JavaScript 函数式编程的基础,也是理解许多框架内部机制的关键。刚开始接触觉得抽象,多写几个例子,慢慢就能体会到它的用处和设计思路。在开发中,当你需要“记住”某个状态、或者创建独立的作用域时,不妨想想闭包能否帮上忙。