一、核心知识点(30 分钟吃透)
1. 闭包核心原理
- 定义:函数嵌套时,内部函数引用外部函数的变量 / 参数,且内部函数被外部访问,导致外部函数执行上下文不被销毁,形成闭包。
- 本质:作用域链的特殊表现 ------ 延长了外部函数变量的生命周期。
- 关键条件 :
- 函数嵌套(内外层函数);
- 内层函数引用外层函数的变量 / 参数;
- 内层函数被外层作用域之外的地方调用。
2. 立即执行函数(IIFE)
-
定义:声明后立即执行的函数,ES5 中主要用于创建独立作用域,避免变量污染(ES6 后可被块级作用域替代,但仍需掌握)。
-
语法 :
js
// 两种常用写法 (function() { /* 代码 */ })(); (function() { /* 代码 */ }()); -
核心作用:隔离作用域,防止变量泄露到全局(如早期 jQuery 源码大量使用)。
3. 闭包的用途、优缺点
表格
| 核心用途 | 优点 | 缺点 |
|---|---|---|
| 1. 保存私有变量(模块化) | 变量私有化,避免全局污染 | 变量不销毁,长期占用内存 |
| 2. 延长变量生命周期 | 可缓存数据,减少重复计算 | 滥用易导致内存泄漏 |
| 3. 实现柯里化 / 防抖节流 | 增强函数灵活性,复用逻辑 | 调试难度增加(作用域链复杂) |
二、手写闭包案例(40 分钟必练)
案例 1:基础闭包(保存私有变量)
js
// 需求:创建计数器,每次调用加1,且计数变量不暴露到全局
function createCounter() {
let count = 0; // 外层变量,被内层函数引用
// 内层函数作为返回值,被外部访问,形成闭包
return function() {
count++;
console.log(count);
};
}
const counter = createCounter();
counter(); // 输出1
counter(); // 输出2
console.log(count); // 报错(count私有化,外部无法访问)
案例 2:闭包实现数据缓存
js
// 需求:缓存计算结果,避免重复计算(如斐波那契数列)
function createFibCache() {
const cache = {}; // 缓存对象,闭包保存
return function fib(n) {
if (n <= 2) return 1;
if (cache[n]) return cache[n]; // 命中缓存直接返回
cache[n] = fib(n-1) + fib(n-2); // 未命中则计算并缓存
return cache[n];
};
}
const fib = createFibCache();
console.log(fib(10)); // 首次计算,缓存结果
console.log(fib(10)); // 直接读取缓存,性能提升
案例 3:立即执行函数 + 闭包(模块化)
js
// 需求:模拟模块化,封装用户信息操作,仅暴露指定方法
const userModule = (function() {
// 私有变量(闭包保存,外部无法直接访问)
let username = "zhangsan";
let age = 20;
// 私有方法
function checkAge() {
return age >= 18;
}
// 暴露公有方法(闭包引用私有变量)
return {
getUsername: function() {
return username;
},
setAge: function(newAge) {
if (newAge > 0) age = newAge;
},
isAdult: function() {
return checkAge();
}
};
})();
console.log(userModule.getUsername()); // zhangsan
userModule.setAge(25);
console.log(userModule.isAdult()); // true
console.log(userModule.username); // undefined(私有变量不可访问)
案例 4:闭包实现防抖(高频面试手写题)
js
// 需求:防抖函数------频繁触发时,仅最后一次触发生效
function debounce(fn, delay) {
let timer = null; // 闭包保存timer,延长生命周期
return function(...args) {
clearTimeout(timer); // 清除上一次的定时器
timer = setTimeout(() => {
fn.apply(this, args); // 绑定this和参数
}, delay);
};
}
// 使用示例
const input = document.querySelector("input");
input.oninput = debounce(function(e) {
console.log("搜索:", e.target.value);
}, 500);
<script>
//案例 一:基础闭包(保存私有变量)
// 需求:创建计数器,每次调用加1,且计数变量不暴露到全局
function createCounter() {
let count = 0;
return function () {
count++;
console.log(count);
};
}
const counter = createCounter();
counter(); // 1
counter(); // 2
console.log(count); // 报错(count私有化,外部无法访问)
// 案例 二:闭包实现数据缓存
// 需求:缓存计算结果,避免重复计算(如斐波那契数列)
// 1. 创建缓存工厂函数
function createFibCache() {
// cache 是私有变量,外部无法直接访问
const cache = {};
// 2. 返回内部函数(形成闭包)
return function fib(n) {
// 边界条件:前两个数都是 1
if (n <= 2) return 1;
// 3. 缓存命中:直接返回,不用重复计算
if (cache[n]) return cache[n];
// 4. 缓存未命中:计算并保存结果
cache[n] = fib(n - 1) + fib(n - 2);
return cache[n];
};
}
// 5. 获取缓存函数
const fib = createFibCache();
// 6. 首次调用:计算并缓存
console.log(fib(10)); // 输出 55,同时缓存了 fib(1)~fib(10)
// 7. 再次调用:直接读缓存,瞬间返回
console.log(fib(10)); // 输出 55,无需重新计算
// 案例 三:立即执行函数 + 闭包(模块化)
// IIFE 创建独立作用域 + 闭包保存私有变量 = 模块化封装,只暴露必要接口,保护内部数据。
// 需求:模拟模块化,封装用户信息操作,仅暴露指定方法
const userModule = (function () {
// 私有变量(闭包保存,外部无法直接访问)
let username = "zhangsan";
let age = 20;
// 私有方法
function checkAge() {
return age >= 18;
}
// 暴露公有方法(闭包引用私有变量)
return {
getUsername: function () {
return username;
},
setAge: function (newAge) {
if (newAge > 0) age = newAge;
},
isAdult: function () {
return checkAge();
},
};
})();
console.log(userModule.getUsername()); // zhangsan
userModule.setAge(25);
console.log(userModule.isAdult()); // true
console.log(userModule.username); // undefined(私有变量不可访问)
// 案例 四:闭包实现防抖(高频面试手写题)
// 需求:防抖函数------频繁触发时,仅最后一次触发生效
function debounce(fn, delay) {
let timer = null; // timer闭包保存timer,延长生命周期
return function (...args) {
if (timer) clearTimeout(timer); // // 清除上一次的定时器
timer = setTimeout(() => {
fn.apply(this, args); // 延迟执行fn
}, delay);
};
}
// 示例:实现输入框实时搜索
const searchInput = document.querySelector("input");
serchInput.oninput = debounce(function (e) {
console.log(e.target.value); // 输入框实时搜索
}, 500);
// 案例 五:闭包实现节流(高频面试手写题)
// 需求:节流函数------频繁触发时,仅在一定时间内执行一次
function throttle(fn, delay) {
let lastTime = 0; // lastTime闭包保存上次执行时间
return function (...args) {
const now = Date.now();
if (now - lastTime >= delay) {
fn.apply(this, args); // 执行fn
lastTime = now; // 更新上次执行时间
}
};
}
// 示例:实现鼠标移动事件的节流
window.onmousemove = throttle(function (e) {
console.log(e.clientX, e.clientY); // 鼠标移动事件节流
}, 500);
// 案例 六:闭包实现函数防重(高频面试手写题)
// 需求:函数防重------同一时间内,同一函数只执行一次
function once(fn) {
let done = false; // done闭包保存执行状态
return function (...args) {
if (!done) {
done = true;
fn.apply(this, args);
}
};
}
// 示例:实现按钮点击防重
const btn = document.querySelector("button");
btn.onclick = once(function () {
console.log("点击成功"); // 按钮点击防重
});
// 案例 七:闭包实现函数缓存(高频面试手写题)
// 需求:函数缓存------同一函数,缓存执行结果,避免重复执行
function cached(fn) {
const cache = {}; // cache闭包保存执行结果
return function (...args) {
const key = JSON.stringify(args); // 缓存key
if (cache[key]) return cache[key]; // 命中缓存,直接返回
const result = fn.apply(this, args); // 执行函数
cache[key] = result; // 缓存结果
return result;
};
}
// 示例:实现数组去重
const arr = [1, 2, 3, 2, 1, 4, 5, 4, 6, 5];
const uniqueArr = [...new Set(arr)]; // 去重
console.log(uniqueArr); // [1, 2, 3, 4, 5, 6]
</script>
三、核心面试题 + 标准答案(20 分钟背会)
面试题:什么是闭包?实际开发中的用途有哪些?
标准答案(高级工程师视角,精简且有深度)
1. 闭包的定义
闭包是 JavaScript 作用域链的特殊表现:当内层函数引用外层函数的变量 / 参数,且内层函数被外层作用域之外的地方调用时,外层函数的执行上下文不会被垃圾回收机制销毁,从而形成闭包。其核心是「延长了外部变量的生命周期,同时实现变量私有化」。
2. 实际开发中的核心用途
(结合业务场景,避免纯理论,体现工程化思维)
- ① 变量私有化与模块化 :在 ES6 Module 普及前,通过闭包 + 立即执行函数封装模块(如工具库、业务组件),将核心变量隐藏,仅暴露指定方法,避免全局变量污染。例如封装用户信息管理模块,只对外提供
getUser/setUser方法,不暴露原始用户数据。 - ② 数据缓存 / 性能优化:对计算成本高的操作(如大数据遍历、递归计算),用闭包缓存结果,避免重复计算。例如斐波那契数列、商品价格计算缓存。
- ③ 实现防抖 / 节流 / 柯里化:前端高频交互场景(输入框搜索、窗口 resize、滚动事件)中,用闭包保存定时器(防抖)、触发时间(节流),控制函数执行频率;柯里化则通过闭包保存部分参数,实现函数参数复用。
- ④ 延长变量生命周期:例如计数器功能,用闭包保存计数变量,每次调用都能基于上次结果更新,且变量不暴露到全局。
3. 加分补充(体现深度)
- 闭包并非 "漏洞",而是语言特性,合理使用能提升代码封装性;
- 实际开发中需注意内存管理:使用完闭包后,手动将引用置为
null(如timer = null),避免内存泄漏; - ES6 后虽可用
let/const块级作用域替代部分闭包场景,但闭包仍是实现复杂逻辑(如防抖节流、自定义 hooks)的核心基础。
四、错题整理模板(10 分钟完成)
表格
| 错题类型 | 错误代码 / 场景 | 错误原因 | 正确思路 / 知识点 |
|---|---|---|---|
| 闭包内存泄漏 | 防抖函数中未清空 timer | 闭包保存的 timer 一直存在,占用内存 | 每次触发前清除 timer,不用时置 null |
| 闭包理解偏差 | 认为 "只要函数嵌套就是闭包" | 忽略 "内层函数被外部访问" 条件 | 闭包必须满足 3 个核心条件 |
| IIFE 使用错误 | function(){}()(无括号) |
普通函数声明无法直接执行 | IIFE 需用括号包裹函数体,触发执行 |
总结
- 核心考点:闭包的定义要紧扣「作用域链 + 变量引用 + 外部访问」,用途需结合业务场景(而非纯理论);
- 实战重点:防抖 / 节流、模块化是闭包最核心的业务落地场景,必须能手写;
- 易错点:闭包的内存泄漏问题,面试中需主动提及 "合理销毁引用" 的优化方案,体现工程化思维。
闭包必须满足 3 个核心条件
- 函数嵌套结构 :必须存在外层函数 和内层函数的嵌套关系(这是闭包形成的结构基础);
- 变量引用关系 :内层函数必须主动引用外层函数的变量 / 参数(而非全局变量,这是闭包形成的核心关联);
- 外部访问条件 :内层函数被外层函数的作用域外部调用 / 保存(如作为返回值、赋值给全局变量、传入其他函数),导致外层函数执行上下文无法被垃圾回收(这是闭包能 "生效" 的关键)。
补充(面试加分)
-
三个条件缺一不可:仅函数嵌套但无变量引用,或仅引用变量但内层函数未被外部访问,都无法形成闭包;
-
举例验证: js
// 满足所有条件 → 形成闭包 function outer() { let a = 1; // 外层变量 return function inner() { // 内层函数被外部访问 console.log(a); // 内层引用外层变量 }; } const fn = outer(); fn();
总结
- 闭包的核心条件聚焦「结构、引用、访问」三个维度,是判断是否形成闭包的关键;
- 理解这三个条件,能快速识别代码中的闭包场景,也能解释闭包 "变量不销毁" 的本质原因