深入理解闭包与内存
从执行上下文到作用域链,彻底搞懂闭包的形成机制、应用场景和内存影响
什么是闭包?
定义:闭包(Closure)是指一个函数能够记住并访问其词法作用域,即使这个函数在其词法作用域之外执行。简单说,当内部函数引用了外部函数的变量,且内部函数在外部函数执行完毕后仍然存活时,就形成了闭包。
涉及场景:
- 数据封装:模拟私有变量,外部无法直接访问内部状态
- 函数工厂:根据参数动态生成具有特定行为的函数
- 回调与事件处理:事件监听器、定时器回调天然形成闭包
- 柯里化与偏函数:利用闭包保存部分参数,生成新函数
- 模块模式:IIFE + 闭包实现模块化(ES Module 之前的主流方案)
- React Hooks:
useState、useEffect内部依赖闭包捕获状态
作用:
- 状态持久化:函数执行完毕后,被引用的变量不会被垃圾回收
- 信息隐藏:实现类似面向对象的私有属性和方法
- 函数式编程基石:高阶函数、柯里化、组合都依赖闭包
- 常见陷阱来源:循环中的闭包、内存泄漏是高频面试考点
前置知识:执行上下文
JavaScript 代码执行时,引擎会创建执行上下文(Execution Context)来管理代码的运行环境。
执行上下文的类型
- 全局执行上下文 — 程序启动时创建,只有一个
- 函数执行上下文 — 每次调用函数时创建
- eval执行上下文 — eval() 中的代码(不推荐使用)
执行上下文栈(Call Stack)
javascript
function first() {
console.log('first');
second();
console.log('first end');
}
function second() {
console.log('second');
third();
console.log('second end');
}
function third() {
console.log('third');
}
first();
// 调用栈变化:
// ┌─────────┐
// │ third │ ← 栈顶(当前执行)
// ├─────────┤
// │ second │
// ├─────────┤
// │ first │
// ├─────────┤
// │ global │ ← 栈底
// └─────────┘
// third 执行完 → 弹出
// second 继续 → "second end" → 弹出
// first 继续 → "first end" → 弹出执行上下文的组成(ES2015+)
javascript
ExecutionContext = {
// 1. 词法环境(LexicalEnvironment)
LexicalEnvironment: {
EnvironmentRecord: {
// let、const、函数声明存储在这里
},
OuterReference: null // 指向外部词法环境(作用域链的关键)
},
// 2. 变量环境(VariableEnvironment)
VariableEnvironment: {
EnvironmentRecord: {
// var 声明存储在这里
},
OuterReference: null
},
// 3. this 绑定
ThisBinding: undefined
}作用域与作用域链
词法作用域(静态作用域)
JavaScript 采用词法作用域——函数的作用域在定义时就确定了,而不是在调用时。
javascript
const x = 10;
function foo() {
console.log(x); // 10(查找定义时的外部作用域)
}
function bar() {
const x = 20;
foo(); // 输出 10,不是 20!
}
bar();
// foo 在定义时,外部作用域是全局,所以 x = 10
// 即使在 bar 中调用,也不会使用 bar 的 x作用域链的形成
javascript
const global = 'global';
function outer() {
const outerVar = 'outer';
function middle() {
const middleVar = 'middle';
function inner() {
const innerVar = 'inner';
// 作用域链:inner → middle → outer → global
console.log(innerVar); // 'inner'(自身)
console.log(middleVar); // 'middle'(middle的)
console.log(outerVar); // 'outer'(outer的)
console.log(global); // 'global'(全局的)
}
inner();
}
middle();
}
outer();
// 作用域链示意:
// inner.[[Scope]] = [innerEnv, middleEnv, outerEnv, globalEnv]
// 查找变量时从左到右依次查找闭包的定义
闭包 = 函数 + 函数定义时的词法环境
当一个函数能够访问其外部函数的变量,即使外部函数已经执行完毕(执行上下文已出栈),这就形成了闭包。
javascript
function createCounter() {
let count = 0; // 自由变量
return function increment() {
count++;
return count;
};
// createCounter 执行完毕后,count 本应被销毁
// 但 increment 函数引用了 count → 闭包形成
// count 被保留在内存中
}
const counter = createCounter();
console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3
// counter 持有对 increment 的引用
// increment 的 [[Scope]] 中持有 createCounter 的词法环境
// 该词法环境中有 count 变量
// 因此 count 不会被垃圾回收闭包的本质
javascript
function outer() {
const a = 1;
const b = 2; // b 没有被内部函数引用
return function inner() {
return a; // 只引用了 a
};
}
const fn = outer();
// V8引擎的优化:
// 闭包只保留被引用的变量(a),未被引用的变量(b)会被回收
// 这称为"闭包变量精简"
// 但使用 eval 时,引擎无法确定哪些变量被引用,会保留所有变量闭包的经典应用
1. 数据封装(模拟私有变量)
javascript
function createBankAccount(initialBalance) {
let balance = initialBalance; // 私有变量
const transactions = []; // 私有变量
return {
deposit(amount) {
if (amount <= 0) throw new Error('金额必须大于0');
balance += amount;
transactions.push({ type: 'deposit', amount, date: new Date() });
return balance;
},
withdraw(amount) {
if (amount > balance) throw new Error('余额不足');
balance -= amount;
transactions.push({ type: 'withdraw', amount, date: new Date() });
return balance;
},
getBalance() {
return balance; // 只能通过方法访问
},
getTransactions() {
return [...transactions]; // 返回副本,防止外部修改
}
};
}
const account = createBankAccount(1000);
account.deposit(500);
account.withdraw(200);
console.log(account.getBalance()); // 1300
// account.balance → undefined(无法直接访问)2. 函数工厂
javascript
function createMultiplier(multiplier) {
return function(number) {
return number * multiplier;
};
}
const double = createMultiplier(2);
const triple = createMultiplier(3);
const tenTimes = createMultiplier(10);
console.log(double(5)); // 10
console.log(triple(5)); // 15
console.log(tenTimes(5)); // 50
// 每次调用 createMultiplier 都创建一个新的闭包
// 每个闭包有自己独立的 multiplier3. 缓存(记忆化)
javascript
function memoize(fn) {
const cache = new Map(); // 闭包中的缓存
return function(...args) {
const key = JSON.stringify(args);
if (cache.has(key)) {
console.log('cache hit');
return cache.get(key);
}
const result = fn.apply(this, args);
cache.set(key, result);
return result;
};
}
const expensiveCalculation = memoize((n) => {
console.log('computing...');
let result = 0;
for (let i = 0; i < n; i++) result += Math.sqrt(i);
return result;
});
expensiveCalculation(1000000); // 'computing...' → 计算
expensiveCalculation(1000000); // 'cache hit' → 直接返回4. 防抖与节流
javascript
// 防抖(闭包保存 timer)
function debounce(fn, delay) {
let timer = null; // 闭包变量
return function(...args) {
clearTimeout(timer);
timer = setTimeout(() => {
fn.apply(this, args);
}, delay);
};
}
// 节流(闭包保存上次执行时间)
function throttle(fn, interval) {
let lastTime = 0; // 闭包变量
return function(...args) {
const now = Date.now();
if (now - lastTime >= interval) {
lastTime = now;
fn.apply(this, args);
}
};
}5. 迭代器
javascript
function createIterator(array) {
let index = 0; // 闭包保存迭代状态
return {
next() {
if (index < array.length) {
return { value: array[index++], done: false };
}
return { done: true };
},
reset() {
index = 0;
}
};
}
const it = createIterator([10, 20, 30]);
console.log(it.next()); // { value: 10, done: false }
console.log(it.next()); // { value: 20, done: false }
console.log(it.next()); // { value: 30, done: false }
console.log(it.next()); // { done: true }6. 单例模式
javascript
const createSingleton = (function() {
let instance = null; // 闭包保存唯一实例
return function(config) {
if (!instance) {
instance = {
config,
createdAt: new Date(),
// ...
};
}
return instance;
};
})();
const a = createSingleton({ name: 'A' });
const b = createSingleton({ name: 'B' });
console.log(a === b); // true(同一个实例)经典面试题
题目1:循环中的闭包
javascript
// 问题代码
for (var i = 0; i < 5; i++) {
setTimeout(function() {
console.log(i);
}, i * 1000);
}
// 输出:5 5 5 5 5
// 原因:var 是函数作用域,5个回调共享同一个 i
// setTimeout 执行时,循环已结束,i = 5
// 解决方案1:IIFE 创建闭包
for (var i = 0; i < 5; i++) {
(function(j) {
setTimeout(function() {
console.log(j);
}, j * 1000);
})(i); // 每次循环创建新的作用域,j 是 i 的副本
}
// 输出:0 1 2 3 4
// 解决方案2:let 块级作用域(推荐)
for (let i = 0; i < 5; i++) {
setTimeout(function() {
console.log(i);
}, i * 1000);
}
// 输出:0 1 2 3 4
// let 在每次循环迭代中创建新的绑定
// 解决方案3:setTimeout 第三个参数
for (var i = 0; i < 5; i++) {
setTimeout(function(j) {
console.log(j);
}, i * 1000, i); // 第三个参数传递给回调
}题目2:闭包与this
javascript
const obj = {
name: '张三',
getName: function() {
return function() {
return this.name;
};
}
};
console.log(obj.getName()()); // undefined(this指向window/global)
// 内部函数是独立调用的,this不指向obj
// 修复方案1:箭头函数
const obj2 = {
name: '张三',
getName: function() {
return () => this.name; // 箭头函数继承外部this
}
};
console.log(obj2.getName()()); // '张三'
// 修复方案2:闭包保存this
const obj3 = {
name: '张三',
getName: function() {
const self = this; // 闭包捕获this
return function() {
return self.name;
};
}
};
console.log(obj3.getName()()); // '张三'题目3:闭包共享变量
javascript
function createFunctions() {
const result = [];
for (var i = 0; i < 3; i++) {
result.push(function() {
return i;
});
}
return result;
}
const fns = createFunctions();
console.log(fns[0]()); // 3(不是0!)
console.log(fns[1]()); // 3
console.log(fns[2]()); // 3
// 三个函数共享同一个闭包中的 i,循环结束后 i = 3题目4:闭包与垃圾回收
javascript
function heavyClosure() {
const hugeData = new Array(1000000).fill('*'); // 大数据
return function() {
// 只用了 hugeData.length,但整个 hugeData 都被保留
return hugeData.length;
};
}
const fn = heavyClosure();
// hugeData 无法被回收,因为 fn 的闭包引用了它
// 优化:只保存需要的数据
function optimizedClosure() {
const hugeData = new Array(1000000).fill('*');
const length = hugeData.length; // 提取需要的值
return function() {
return length; // 只引用了 length
};
// hugeData 没有被闭包引用,可以被GC回收
}闭包与内存泄漏
什么是内存泄漏?
不再需要的内存没有被释放,导致内存持续增长。
场景1:被遗忘的定时器
javascript
// ❌ 内存泄漏
function startPolling() {
const data = fetchHugeData();
setInterval(() => {
// 闭包引用了 data
process(data);
}, 1000);
// 如果不 clearInterval,data 永远无法回收
}
// ✅ 正确做法
function startPolling() {
const data = fetchHugeData();
const timer = setInterval(() => {
process(data);
}, 1000);
// 提供清理方法
return function stop() {
clearInterval(timer);
// timer 被清除后,回调函数被释放,data 可以被回收
};
}
const stopPolling = startPolling();
// 不需要时调用 stopPolling()场景2:事件监听器未移除
javascript
// ❌ 内存泄漏
function setupHandler() {
const element = document.getElementById('button');
const data = loadHugeData();
element.addEventListener('click', function handler() {
console.log(data); // 闭包引用 data
});
// 元素被移除,但事件监听器和闭包仍然存在
}
// ✅ 正确做法
function setupHandler() {
const element = document.getElementById('button');
const data = loadHugeData();
function handler() {
console.log(data);
}
element.addEventListener('click', handler);
// 返回清理函数
return function cleanup() {
element.removeEventListener('click', handler);
};
}
// 或使用 AbortController
function setupHandler() {
const controller = new AbortController();
const data = loadHugeData();
element.addEventListener('click', () => {
console.log(data);
}, { signal: controller.signal });
return () => controller.abort(); // 一键移除所有监听器
}场景3:闭包循环引用
javascript
// ❌ 旧版IE中的经典泄漏(现代浏览器已解决)
function leak() {
const element = document.getElementById('box');
element.onclick = function() {
// 闭包引用了 element
console.log(element.id);
};
// element → onclick → 闭包 → element(循环引用)
}
// ✅ 解决方案
function noLeak() {
const element = document.getElementById('box');
const id = element.id; // 提取需要的值
element.onclick = function() {
console.log(id); // 不再引用 element
};
}场景4:意外的全局变量
javascript
// ❌ 意外创建全局变量
function process() {
// 忘记 var/let/const → 自动创建全局变量
tempData = new Array(1000000).fill('*');
}
// tempData 永远不会被回收
// ✅ 使用严格模式
'use strict';
function process() {
tempData = []; // ❌ ReferenceError: tempData is not defined
}场景5:console.log 保持引用
javascript
// ❌ 开发时的陷阱
function createData() {
const data = new Array(1000000).fill('*');
console.log(data); // DevTools 保持对 data 的引用!
return data.length;
}
// 生产环境应移除 console.log
// 或使用构建工具(如 terser)自动移除场景6:DOM引用未清除
javascript
// ❌ 保存DOM引用
const cache = {};
function addToCache(id) {
cache[id] = document.getElementById(id);
}
addToCache('header');
// 即使 #header 从DOM中移除,cache.header 仍然引用它
// → Detached DOM tree(分离的DOM树)
// ✅ 使用 WeakRef
const cache2 = {};
function addToCache2(id) {
const el = document.getElementById(id);
cache2[id] = new WeakRef(el);
}
function getFromCache(id) {
const ref = cache2[id];
if (!ref) return null;
const el = ref.deref();
if (!el) {
delete cache2[id]; // 已被GC
return null;
}
return el;
}Chrome DevTools 内存分析
检测内存泄漏的步骤
1. Performance Monitor(性能监视器)
打开方式:DevTools → Ctrl+Shift+P → "Performance Monitor"
观察:JS Heap Size 是否持续增长
2. Memory → Heap Snapshot
- 拍摄快照1
- 执行可能泄漏的操作
- 拍摄快照2
- 对比两个快照(Comparison视图)
- 查看 Delta 列中新增的对象
3. Memory → Allocation instrumentation on timeline
- 实时观察内存分配
- 蓝色条 = 分配但未回收的内存
- 灰色条 = 已回收的内存
4. 关键指标
- Shallow Size: 对象自身占用的内存
- Retained Size: 对象及其引用的所有对象占用的总内存
- Detached DOM tree: 已从页面移除但仍在内存中的DOM节点手动触发GC
javascript
// Chrome DevTools 中:
// Memory 面板 → 点击垃圾桶图标
// 或在 Timeline/Performance 面板录制时
// 点击 "Collect garbage" 按钮
// Node.js 中:
// node --expose-gc script.js
// global.gc(); // 手动触发GC闭包的性能考量
javascript
// 1. 闭包创建有开销(但现代引擎已高度优化)
// 避免在热路径中不必要地创建闭包
// ❌ 不必要的闭包
function processArray(arr) {
return arr.map(function(item) { // 每次 processArray 调用都创建新函数
return item * 2;
});
}
// ✅ 复用函数
const double = item => item * 2;
function processArray(arr) {
return arr.map(double); // 复用同一个函数
}
// 2. 闭包变量的访问比局部变量慢(作用域链查找)
// 但差异极小,通常不需要担心
// 3. 大量闭包共享大对象时注意内存
function createHandlers(elements) {
const sharedData = loadHugeData(); // 所有闭包共享
// ❌ 所有 handler 都持有 sharedData 的引用
return elements.map(el => () => process(el, sharedData));
// ✅ 提取每个元素需要的数据
return elements.map(el => {
const needed = extractNeeded(sharedData, el);
return () => process(el, needed);
});
}总结
闭包核心知识点:
┌──────────────────────────────────────────────────────────┐
│ 定义 │
│ • 闭包 = 函数 + 定义时的词法环境 │
│ • 内部函数引用了外部函数的变量 → 形成闭包 │
│ • 外部函数执行完毕后,被引用的变量不会被销毁 │
├──────────────────────────────────────────────────────────┤
│ 形成条件 │
│ • JavaScript 的词法作用域(定义时确定,非调用时) │
│ • 作用域链:内部 → 外部 → ... → 全局 │
│ • 函数是一等公民(可以作为值传递) │
├──────────────────────────────────────────────────────────┤
│ 应用场景 │
│ • 数据封装(模拟私有变量) │
│ • 函数工厂 / 柯里化 │
│ • 缓存(记忆化) │
│ • 防抖 / 节流 │
│ • 模块模式 / 单例模式 │
├──────────────────────────────────────────────────────────┤
│ 内存泄漏防范 │
│ • 及时 clearTimeout/clearInterval │
│ • removeEventListener 或用 AbortController │
│ • 避免不必要的大对象引用 │
│ • 生产环境移除 console.log │
│ • 用 WeakRef/WeakMap 做缓存 │
│ • Chrome DevTools Memory 面板排查 │
└──────────────────────────────────────────────────────────┘