
基础知识复习- 关于Js中闭包的详细学习
核心定义
闭包(Closure)是 函数与其词法环境的组合,使内部函数能够访问并保留其外部函数作用域中的变量,即使外部函数已执行完毕。从实现角度看,闭包是函数内部定义的子函数与外部变量之间的“桥梁”,允许跨作用域的数据访问。
形成条件
- 存在嵌套函数:外层函数内定义内层函数。
- 内部函数引用外部变量:内层函数需访问外层函数的局部变量或参数。
- 外部函数返回内部函数:通过返回值将闭包暴露到外层作用域。
特性
- 封装性:通过闭包隐藏变量,模拟私有属性(如计数器、缓存)。
- 持久性:闭包中的变量生命周期与闭包本身绑定,不受外层函数执行周期限制。
核心机制
- 词法作用域:闭包的变量访问规则由函数定义时的位置决定,而非执行时的作用域。
- 持久化环境:外层函数执行完毕后,其变量因被内层函数引用而无法被垃圾回收,形成长期驻留内存的状态。
示例:
function outer() {
let count = 0;
return function inner() {
count++;
return count;
};
}
const counter = outer();
console.log(counter()); // 1
console.log(counter()); // 2
此例中,inner
函数通过闭包持续访问并修改外层 count
变量,形成独立且持久的状态。
工作原理
- 词法作用域决定变量访问范围。
- 作用域链实现变量逐层查找。
- 执行上下文保留使外部变量持久化。
通过这一机制,闭包能够在函数执行后仍维持对原作用域变量的引用,支持数据封装、状态持久化等高级功能。
词法作用域绑定
闭包的核心机制基于 JavaScript 的词法作用域(静态作用域),即函数的作用域在代码编写阶段确定,而非运行时动态生成。函数在定义时即会记录其所在作用域的变量环境,即使函数在其词法作用域之外执行,仍能访问这些变量。
function outer() {
let x = 10;
function inner() {
console.log(x); // 访问外部作用域的变量
}
return inner;
}
const func = outer();
func(); // 输出 10(x 仍可访问)
这里,inner
函数在 outer
函数执行后仍能通过闭包访问。
作用域链机制
闭包通过作用域链逐级向上查找变量:
每个函数在创建时会生成一个作用域链,包含自身作用域及所有外层作用域的变量环境。当内部函数访问变量时,会先在自身作用域查找,若未找到则沿作用域链向外层作用域查找。
function outer() {
let y = 20;
return function inner() {
return y; // 通过作用域链访问外层变量
};
}
此时,inner
函数的作用域链包含 outer
的作用域,因此能访问 y
。
执行上下文与变量持久化
闭包的持久化依赖于执行上下文的保留:
当外部函数执行完毕后,其执行上下文通常会被销毁,但若内部函数引用了外部函数的变量,则这些变量会被保留在内存中(形成闭包)。
function createCounter() {
let count = 0;
return function() {
return ++count;
};
}
const counter = createCounter();
counter(); // 1(count 未被回收)
此处,count
变量因被闭包引用而长期驻留内存,直至闭包被销毁。
实现过程
- 函数嵌套与变量引用触发闭包捕获。
- 作用域链保留确保外部变量持久化。
- 内存管理机制控制变量的生命周期。
从理解触发对工作原理的定义-闭包在 JavaScript 中的实现过程基于词法作用域和作用域链机制。
函数嵌套与变量引用
函数嵌套:内部函数定义在外部函数体内,形成嵌套结构。
变量绑定:内部函数需直接引用外部函数的变量或参数,此时外部变量被闭包“捕获”。
function outer() {
let count = 0;
function inner() {
count++;
return count;
}
return inner;
}
此时代码中,inner
函数引用了外部变量 count
。
执行上下文与作用域链的保留
外部函数执行:调用 outer()
时,创建其执行上下文,包含变量 count
和作用域链。
内部函数创建:inner
函数在定义时记录其词法环境(即 outer
的作用域链)。
跨作用域传递:当 outer
执行完毕并返回 inner
函数时,outer
的执行上下文理论上应销毁,但因 inner
仍引用 count
,该变量被保留在内存中。
闭包的实际运行
闭包调用:将返回的 inner
函数赋值给变量(如 const counter = outer()
),后续调用 counter()
时,沿作用域链查找 count
,确认其存在于闭包保留的 outer
作用域中。修改 count
的值并返回,实现状态持久化。
内存驻留:只要闭包存在(如 counter
未被释放),count
变量便不会被垃圾回收。
内存管理机制
变量引用计数:JavaScript 引擎通过检查变量是否被闭包引用,决定是否回收内存。
手动释放:将闭包变量置为 null
(如 counter = null
),可主动触发垃圾回收。
let closure = (function() {
let data = "敏感数据";
return {
getData: () => data
};
})();
closure = null; // 解除引用,释放内存
此操作可避免内存泄漏。
代码实现示例
基础闭包实现
闭包的核心是内部函数引用外部变量,且外部函数执行后变量仍驻留内存。
function createCounter() {
let count = 0; // 外部函数的变量
return function() {
count++;
return count;
};
}
const counter = createCounter();
console.log(counter()); // 输出 1
console.log(counter()); // 输出 2
说明:createCounter
返回的匿名函数通过闭包保留了 count
变量,每次调用 counter()
都会更新 count
的值。
循环中的闭包
在循环中直接创建闭包可能导致变量共享问题。
const funcs = [];
for (var i = 0; i < 3; i++) {
funcs.push(function() {
console.log(i);
});
}
funcs(); // VM68:7 Uncaught TypeError: funcs is not a function at :7:1
funcs.forEach(func => func()); // 输出 3、3、3
原因:
- 调用方式错误:
funcs
是数组而非函数,直接调用funcs()
会报错TypeError: funcs is not a function
。 - 所有闭包共享同一个
i
(var
无块级作用域)。 - 所有闭包共享全局变量
i
(循环结束后i = 3
),因此输出相同值。
修复方法(使用 IIFE 或 let
):
// 方法1:使用 IIFE
const funcs = [];
for (var i = 0; i < 3; i++) {
(function(j) {
funcs.push(function() {
console.log(j);
});
})(i);
}
funcs.forEach(func => func());
// 方法2:使用 let(块级作用域)
const funcs = [];
for (let i = 0; i < 3; i++) {
funcs.push(function() {
console.log(i);
});
}
funcs.forEach(func => func());
说明:两种方法均通过隔离作用域使每个闭包捕获独立的 i
值。
模块模式封装私有变量
通过闭包实现数据私有化:
const module = (function() {
let privateData = "私有数据";
function privateMethod() {
console.log(privateData);
}
return {
publicMethod: function() {
privateMethod();
}
};
})();
module.publicMethod(); // 输出 “私有数据”
console.log(module.privateData); // undefined(无法访问)
说明:立即执行函数(IIFE)返回包含公共方法的对象,私有变量 privateData
仅通过闭包暴露接口。
闭包实现状态保留
在DOM事件处理中保持状态:
function setupButtons() {
const buttons = document.querySelectorAll("button");
for (let i = 0; i < buttons.length; i++) {
buttons[i].addEventListener("click", function() {
console.log(`按钮 ${i} 被点击`);
});
}
}
说明:使用 let
为每个按钮事件回调生成独立闭包,正确绑定对应的索引 i
。
通过以上代码,我们可以简单的对闭包有一个简单的理解。现在我们详细的分析闭包问题的拆分,以上就可以应付八股面试,以下是对闭包的一些简陋的认识。
词法作用域对闭包的影响机制
词法作用域通过固化变量查找路径和延长变量生命周期,为闭包提供基础运行环境。闭包利用这一机制实现跨作用域数据持久化,但其变量共享问题需通过作用域隔离技术(如 let
、IIFE)规避。
概念
词法作用域(静态作用域):函数的作用域在代码编写时由其物理位置决定,而非运行时动态确定。
闭包:函数能够持续访问其定义时所处词法环境中的变量,即使该函数在原始作用域外执行。
影响闭包的核心机制
作用域链固化:闭包通过词法作用域锁定变量查找路径,形成包含外部变量的作用域链。即使外部函数执行完毕,闭包仍能通过固化链访问变量,也就是我们上文中的持久化环境。
function outer() {
let x = 10;
function inner() {
console.log(x); // 通过词法作用域访问外层x
}
return inner;
}
const closure = outer();
closure(); // 输出10(闭包保留x的引用)
变量生命周期延长:词法作用域使闭包引用的变量脱离原始作用域销毁规则,只要闭包存在,变量就不会被垃圾回收。
典型表现
变量共享问题:若闭包依赖的词法作用域中存在循环变量(如 var
声明的全局变量),所有闭包共享同一变量,导致最终值相同。在上文中循环中的闭包就出现了这样的场景。
设计意义与限制
优势:词法作用域的静态特性使闭包行为可预测,便于封装私有变量和维持状态。
风险:过度依赖闭包可能导致内存泄漏(如意外保留大对象引用)或调试困难(作用域链复杂化)。
JavaScript 闭包、执行上下文与作用域链的作用
三者的协作关系
执行上下文生成作用域链:函数执行时,其执行上下文包含作用域链,作为变量查找的依据。
词法作用域决定作用域链结构:函数定义时的词法环境固化作用域链层级,与运行时调用位置无关。
闭包依赖作用域链实现数据持久化:闭包通过固化后的作用域链访问外部变量,即使外层上下文已销毁。
执行上下文:动态管理代码运行环境,存储变量、作用域链及 this
。
作用域链:静态固化变量查找路径,支持闭包和词法作用域隔离。
闭包:通过作用域链实现跨作用域数据持久化和封装私有变量。
三者共同构成 JavaScript 变量管理和闭包功能的核心机制。
执行上下文的作用
管理代码执行环境:执行上下文是 JavaScript 代码运行时的核心容器,存储当前环境的变量对象(VO/AO)、作用域链及 this
绑定。每次函数调用或全局代码执行时,会创建新的执行上下文,并按“后进先出”规则压入执行栈。
控制变量与函数生命周期:在创建阶段,执行上下文预解析变量和函数声明,初始化变量为 undefined
,并绑定作用域链。函数执行完毕后,其执行上下文从栈中弹出,但闭包引用的变量可能保留在内存中。
具体可看 JavaScript执行上下文。
闭包的7大核心应用场景
封装私有变量与方法
通过闭包隐藏内部变量,仅暴露特定接口,防止外部直接修改数据,提升代码安全性和可维护性。
示例:模块化开发中创建独立作用域,避免全局污染。
IIFE(立即执行函数表达式)
通过自执行函数创建独立作用域,变量仅在闭包内有效:
const myModule = (function() {
let privateVar = '内部数据'; // 私有变量
function privateMethod() { // 私有方法
console.log(privateVar);
}
return { // 暴露的公共接口
publicMethod: function() {
privateMethod();
}
};
})();
myModule.publicMethod(); //内部数据
模块模式(Module Pattern)
结合闭包与对象返回,支持多实例化:
function createModule() {
let state = 0; // 私有状态
return {
increment: () => ++state,
getState: () => state
};
}
const mod1 = createModule();
const mod2 = createModule();
mod1.increment();
命名空间增强
在闭包中构建模块层级,防止全局对象膨胀:
(function(namespace) {
let config = { env: 'prod' }; // 私有配置
namespace.getConfig = () => config;
})(window.APP = window.APP || {});
console.log(APP.getConfig().env);
高阶函数与函数工厂
生成可定制化函数(如参数化函数模板)或实现装饰器模式。
示例:函数返回自增闭包,实现状态持久化。
基础函数工厂案例
通过闭包隔离计数器状态,生成多个独立实例:
function createCounter() {
let count = 0; // 闭包保存私有状态
return function() { // 高阶函数返回新函数
return ++count;
};
}
const counterA = createCounter();
const counterB = createCounter();
console.log(counterA());
console.log(counterB());
带配置参数的增强型工厂
利用高阶函数传递初始化参数,动态生成不同行为的函数:
function createMultiplier(factor) {
return function(num) {
return num * factor; // 闭包保留factor参数
};
}
const double = createMultiplier(2);
const triple = createMultiplier(3);
console.log(double(5)); // 10
模块化开发中的应用
结合IIFE(立即执行函数)实现模块作用域隔离:
const dataService = (function() {
let cache = {}; // 私有变量
return {
fetch: (key) => cache[key] || loadData(key), // 高阶方法
clear: () => cache = {} // 暴露接口
};
})();
dataService;
回调函数与异步编程
在异步操作(如 setTimeout
、AJAX)中保留上下文变量,确保回调函数执行时仍能访问所需数据。
示例:事件处理函数中通过闭包捕获 DOM 元素或事件参数。
function handleClick(element) {
element.addEventListener('click', function() {
setTimeout(() => {
console.log(element.id); // 闭包保留触发事件的DOM对象
}, 1000);
});
}
function createHandler(element, config) {
return function(event) {
element.style.color = config.color; // 闭包保留元素和配置参数
console.log('触发事件:', event.target);
};
}
const btnHandler = createHandler(document.getElementById('btn'), {color: 'red'});
变量状态持久化:闭包通过保留外部函数作用域的变量,确保回调函数执行时能访问到正确的变量值,避免异步操作中因变量被覆盖导致的逻辑错误。
封装私有状态:闭包允许在异步操作中隐藏内部数据,仅通过回调接口暴露必要功能。例如封装网络请求的缓存机制。
function createFetchService() {
let cache = {}; // 私有变量
return function(url, callback) {
if (cache[url]) return callback(cache[url]);
fetch(url).then(res => {
cache[url] = res;
callback(res);
});
};
}
const fetchWithCache = createFetchService();
事件处理:通过闭包保存事件触发时的上下文信息,避免全局变量污染:
document.querySelectorAll('.btn').forEach(btn => {
btn.addEventListener('click', function() {
const currentBtn = this; // 闭包保留当前按钮对象
setTimeout(() => console.log(currentBtn.id), 1000);
});
});
异步流程控制:结合闭包与回调实现顺序执行异步操作。
function asyncSequence(tasks, finalCallback) {
let index = 0;
function next() {
if (index < tasks.length) {
tasks[index++](next); // 闭包保存当前执行进度
} else {
finalCallback();
}
}
next();
}
// 依次执行任务队列
模块化开发
将功能模块的变量和方法封装在闭包中,通过返回接口对象提供外部访问权限,减少全局命名冲突。
模块化工具库开发:如日期处理、数据校验等工具库,通过闭包隔离工具方法,仅暴露必要接口。
延迟执行与状态保留
闭包可保存临时状态(如循环中的索引值),解决异步操作因变量提升导致的逻辑错误。
循环内使用闭包绑定 i
的值,避免异步回调输出重复结果。
数据缓存与性能优化
缓存计算结果(如斐波那契数列),避免重复计算,提升执行效率。
function memoize(fn) {
const cache = new Map(); // 创建缓存容器
return function(...args) {
const key = JSON.stringify(args); // 序列化参数作为缓存键
if (cache.has(key)) return cache.get(key); // 缓存命中
const result = fn(...args); // 原始函数调用
cache.set(key, result); // 缓存计算结果
return result;
};
}
// 应用示例 斐波那契示例解析
const memoizedFib = memoize(function(n) {
return n <= 1 ? n : memoizedFib(n - 1) + memoizedFib(n - 2);
});
console.log(memoizedFib(50)); // 快速计算结果
缓存机制:使用 Map
存储计算结果,JSON.stringify(args)
将参数序列化为唯一缓存键。
闭包应用:返回的函数保持对 cache
的引用,形成闭包。
性能优化:避免重复计算,空间换时间的典型场景。
递归优化:普通递归斐波那契时间复杂度是 O(2ⁿ),记忆化后降为 O(n)。
缓存生效关键:必须通过 memoizedFib
进行递归调用才能触发缓存机制。
计算规模:memoizedFib(50)
在普通递归下不可行(需要约 1.6e11 次运算),记忆化后只需 50 次计算。
注意事项:
- 参数序列化限制:当参数包含循环引用对象时,
JSON.stringify
会失败。 - 引用类型参数:对象参数即使内容相同但引用不同,会被视为不同键值。
- 副作用函数:不适用于有副作用的函数(如修改外部变量),因为缓存会跳过实际执行。
函数柯里化与装饰器:拆分多参数函数为链式调用,或通过装饰器扩展函数功能(如日志记录、权限校验)。
函数柯里化通过闭包保存已传递参数,逐步接收剩余参数,最终完成计算。
// 通用柯里化函数实现
function curry(fn) {
return function curried(...args) {
// 判断当前参数数量是否满足原始函数要求
if (args.length >= fn.length) {
return fn(...args); // 参数足够时执行原始函数
} else {
return (...newArgs) => curried(...args, ...newArgs); // 闭包保存已传参数
}
};
}
// 示例:三参数加法函数柯里化
const add = (a, b, c) => a + b + c;
const curriedAdd = curry(add);
console.log(curriedAdd(1)(2)(3)); // 6(链式调用)
// 参数分步传递
const add5 = curriedAdd(2)(3);
console.log(add5(5)); // 10(2+3+5)
// 组合函数
const doubleAdd = curriedAdd(1)(1);
[1,2,3].map(doubleAdd); // [3,4,5]
- 参数累积:通过闭包保存每次调用的参数。
- 递归返回:每次参数不足时返回新的函数。
- 长度判断:
fn.length
获取原始函数形参个数。 - 函数组合:通过连续返回函数实现链式调用。
示例执行流程
const add = (a, b, c) => a + b + c;
const curriedAdd = curry(add);
// 执行过程分解:
1. curriedAdd(1) → 参数长度 1 < 3 → 返回新函数保存 [1]
2. (2) → 合并参数 [1,2] → 长度 2 < 3 → 返回新函数
3. (3) → 合并参数 [1,2,3] → 触发执行 add(1,2,3)
- 闭包应用:通过嵌套函数保留参数状态。
- 函数式编程:实现函数的延迟执行和参数分步传递。
- 自动参数收集:使用剩余参数语法(…)简化参数处理。
权限校验装饰器
function withAuth(fn) {
return function(...args) {
const user = getCurrentUser(); // 模拟获取用户信息
if (!user?.isAdmin) throw new Error("无权限操作"); // 权限校验逻辑
return fn(...args); // 校验通过后执行原函数
};
}
// 应用示例
const deleteUser = withAuth(function(userId) {
/* 删除用户逻辑 */
});
deleteUser(123); // 非管理员触发异常
功能解耦:核心逻辑与辅助功能(日志、权限)分离,提升代码可维护性。
复用性:同一装饰器可应用于多个函数(如全局日志记录)。
闭包高效使用
闭包内存泄漏核心原因
长期持有外部变量引用:闭包会保留其外层函数的作用域链,若闭包本身长期存活(如被全局变量引用),其引用的外部变量无法被垃圾回收。
循环引用:闭包与外部变量形成相互引用(如闭包引用 DOM 元素,DOM 元素又通过事件监听器引用闭包),导致双方均无法回收。
未及时释放资源:未主动解除闭包对大型对象(如数组、缓存数据)的引用,导致内存长期占用。
高效内存使用策略
及时释放外部变量:闭包使用完毕后,将不再需要的外部变量显式设为 null
,解除强引用。
function createClosure() {
let largeData = new Array(1e6).fill('data'); // 改用 let 声明
return {
useData: function() { // 使用数据
const result = largeData.length; // 使用后立即释放
largeData = null; // ✅ 解除引用
return result;
},
cleanup: function() { // 可选清理方法
largeData = null;
}
};
}
// 在使用完数据后立即置为 null,确保:
const closure = createClosure();
closure.useData(); // 使用数据并自动释放
// 此时 largeData 已被标记为可回收
减少闭包嵌套层级
优先使用局部变量缓存外层变量,减少作用域链遍历次数
function outer() {
const data = computeData();
return function () {
const cachedData = data; // 缓存到局部变量
// 使用 cachedData 操作
};
}
及时释放无用闭包
手动解除闭包对外部变量的引用(如置空变量、移除事件监听)
闭包生命周期的控制方法
闭包生命周期的触发与终止条件
创建时机:当外部函数被调用,且内部函数引用外部作用域的变量时,闭包立即生成。
终止条件:闭包会持续存在,直到所有引用内部函数的变量被解除(如置为 null
或超出作用域),此时闭包及其关联的变量会被垃圾回收(GC)销毁