闭包与作用域
作者:海川,发表于:2026年1月27日 11:21:36
从作用域链到闭包陷阱,深度掌握 JavaScript 的作用域机制、闭包原理、内存管理,包括 10 个实战模式和性能优化
前言
闭包和作用域是 JavaScript 最”玄学”的概念。很多开发者能用,但说不清原理。结果面试被虐,项目里埋雷。
本文从作用域的本质、作用域链的机制、闭包的原理,到10 个实战模式和性能优化,让你真正理解 JavaScript 的作用域和闭包。
一、核心速览(60 秒)
作用域概念
| 类型 | 作用范围 | 存在时间 | 创建方式 |
|---|---|---|---|
| 全局作用域 | 整个程序 | 程序开始到结束 | 全局 |
| 函数作用域 | 函数内部 | 函数调用到返回 | function |
| 块级作用域 | {} 内部 | 块级执行完成 | let/const |
| 动态作用域 | 函数被调用的位置 | 调用时确定 | this |
闭包本质
闭包 = 函数 + 它能访问的外层变量
简单说:一个函数能记住并访问它外层作用域的变量,即使外层函数已经执行完毕。
闭包的三个条件
- 有一个内层函数
- 内层函数引用外层变量
- 外层函数已执行完毕,内层函数仍存在
二、作用域基础
作用域链(Scope Chain)
const global = "global"; // 全局作用域
function outer() {
const outerVar = "outer"; // 函数作用域
function inner() {
const innerVar = "inner"; // 函数作用域
console.log(innerVar); // 'inner'(本层)
console.log(outerVar); // 'outer'(父层)
console.log(global); // 'global'(全局)
}
inner();
}
outer();
访问顺序:
- 先在本层查找
innerVar→ 找到 - 再在父层查找
outerVar→ 找到 - 再在全局查找
global→ 找到 - 如果全部找不到 →
ReferenceError
关键点:变量查找是单向的,内层可以访问外层,但外层无法访问内层。
const x = "global";
function outer() {
const x = "outer";
function inner() {
const x = "inner";
console.log(x); // 'inner'(就近原则)
}
inner();
}
outer();
就近原则:如果多层都有同名变量,使用最近的。
全局作用域
// 全局变量
var globalVar = "var"; // 会成为 window 属性
let globalLet = "let"; // 不会
const globalConst = "const"; // 不会
console.log(window.globalVar); // 'var'
console.log(window.globalLet); // undefined
console.log(window.globalConst); // undefined
问题:全局变量污染,所有函数都能修改。
// ❌ 全局污染
var count = 0;
function increment() {
count++; // 任何地方都能修改
}
// ✅ 避免全局污染
const counter = {
count: 0,
increment() {
this.count++;
},
};
函数作用域 vs 块级作用域
// var:函数作用域(ES5)
function test() {
if (true) {
var x = 1;
}
console.log(x); // 1(泄漏出来)
}
// let/const:块级作用域(ES6)
function test() {
if (true) {
let y = 1;
}
console.log(y); // ReferenceError(不泄漏)
}
// for 循环中特别明显
for (var i = 0; i < 3; i++) {} // var i 是函数级
console.log(i); // 3(污染全局)
for (let j = 0; j < 3; j++) {} // let j 是块级
console.log(j); // ReferenceError
为什么 let/const 更好:
- 防止变量泄漏
- 避免变量提升的混乱
- 减少意外的全局污染
暂时死区(Temporal Dead Zone)
// var:有提升(初始化为 undefined)
console.log(x); // undefined
var x = 1;
// let/const:有提升但未初始化(暂时死区)
console.log(y); // ReferenceError
let y = 1;
// 暂时死区的范围:从块开始到声明处
function test() {
console.log(z); // ReferenceError(在 let z 声明前)
let z = 1;
console.log(z); // 1
}
三、闭包:JavaScript 最强大的特性
什么是闭包?
// 最简单的闭包
function createCounter() {
let count = 0; // 外层变量
return function () {
return ++count; // 内层函数访问外层变量
};
}
const counter = createCounter();
console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3
这就是闭包:
counter是返回的内层函数- 虽然
createCounter已经执行完毕,但counter仍然能访问count - 每次调用
counter,都使用同一个count变量(重要!)
闭包能做什么?
1. 数据封装(私有变量)
// ✅ 模拟"私有变量"
const user = (() => {
let _password = ""; // 私有变量(通常用 _ 前缀)
return {
setPassword(pwd) {
_password = pwd;
},
verifyPassword(pwd) {
return _password === pwd;
},
};
})();
user.setPassword("123456");
console.log(user._password); // undefined(访问不到)
console.log(user.verifyPassword("123456")); // true
2. 工厂函数
// 创建多个独立的对象
function createUser(name) {
let email = ""; // 每个实例都有独立的变量
return {
getName() {
return name;
},
setEmail(e) {
email = e;
},
getEmail() {
return email;
},
};
}
const user1 = createUser("Alice");
const user2 = createUser("Bob");
user1.setEmail("alice@ex.com");
user2.setEmail("bob@ex.com");
console.log(user1.getEmail()); // 'alice@ex.com'
console.log(user2.getEmail()); // 'bob@ex.com'
3. 函数柯里化
// 柯里化:转换多参数函数,重点在于闭包和递归
function curry(fn) {
const arity = fn.length; // 函数形参个数
return function curried(...args) {
// fn的参数传够的时候,执行fn
if (args.length >= arity) {
return fn(...args);
}
// fn的参数没有传够,继续返回接受新参数的函数
return (...nextArgs) => curried(...args, ...nextArgs);
};
}
const add = (a, b, c) => a + b + c;
const curriedAdd = curry(add);
console.log(curriedAdd(1)(2)(3)); // 6
console.log(curriedAdd(1, 2)(3)); // 6
console.log(curriedAdd(1)(2, 3)); // 6
4. 模块模式
// ✅ 模块化:暴露部分功能,隐藏实现细节
const calculator = (() => {
let result = 0; // 私有状态
const add = (n) => (result += n);
const subtract = (n) => (result -= n);
const multiply = (n) => (result *= n);
const getResult = () => result;
// 只暴露需要的接口
return { add, subtract, multiply, getResult };
})();
calculator.add(5);
calculator.multiply(2);
console.log(calculator.getResult()); // 10
console.log(calculator.result); // undefined(无法访问)
四、闭包的常见陷阱
❌ 陷阱 1:循环中的闭包
// ❌ 错误:所有函数都引用同一个 i
const funcs = [];
for (var i = 0; i < 3; i++) {
funcs.push(() => console.log(i));
}
funcs[0](); // 3(不是 0)
funcs[1](); // 3(不是 1)
funcs[2](); // 3(不是 2)
// 原因:循环结束后 i = 3,所有函数都引用这个 i
为什么?
// 执行过程:
// i = 0: 创建函数,闭包保存 i 的引用
// i = 1: 创建函数,闭包保存 i 的引用(同一个 i)
// i = 2: 创建函数,闭包保存 i 的引用(同一个 i)
// i = 3: 循环结束
// 所有函数的 i 都已经是 3
const funcs = [];
for (var i = 0; i < 3; i++) {
// 此时都指向同一个 i 变量
funcs.push(() => console.log(i));
}
解决方案 1:用 let 创建块级作用域
// ✅ 最简单的解决
const funcs = [];
for (let i = 0; i < 3; i++) {
// let 每次创建新的 i
funcs.push(() => console.log(i));
}
funcs[0](); // 0
funcs[1](); // 1
funcs[2](); // 2
解决方案 2:立即执行函数(IIFE)
// ✅ 创建独立作用域
const funcs = [];
for (var i = 0; i < 3; i++) {
funcs.push(
(
(j) => () =>
console.log(j)
)(i),
); // 传参创建新作用域
}
funcs[0](); // 0
funcs[1](); // 1
funcs[2](); // 2
解决方案 3:工厂函数
// ✅ 工厂函数
const funcs = [];
for (var i = 0; i < 3; i++) {
funcs.push(createLogger(i));
}
function createLogger(value) {
return () => console.log(value);
}
funcs[0](); // 0
❌ 陷阱 2:回调中的 this
// ❌ this 指向错误
const obj = {
name: "obj",
getName: function () {
setTimeout(function () {
// 回调函数的执行者是全局Scope,这里直接访问obj.name也行
// 但不如箭头函数严谨
console.log(this.name); // undefined(this 是 window)
}, 100);
},
};
obj.getName();
// ✅ 解决 1:箭头函数(继承外层 this)
const obj = {
name: "obj",
getName: function () {
setTimeout(() => {
console.log(this.name); // 'obj'
}, 100);
},
};
// ✅ 解决 2:保存 this
const obj = {
name: "obj",
getName: function () {
const self = this; // 保存 this
setTimeout(function () {
console.log(self.name); // 'obj'
}, 100);
},
};
❌ 陷阱 3:意外的闭包导致内存泄漏
// ❌ 内存泄漏:大对象被意外引用
function setupListener() {
const largeData = new Array(1000000).fill("data");
document.getElementById("btn").addEventListener("click", () => {
console.log(largeData[0]); // 闭包引用了整个大数组
});
}
// largeData 永远不会被垃圾回收,因为监听器仍在运行
// ✅ 解决:显式清理
function setupListener() {
const largeData = new Array(1000000).fill("data");
const handler = () => {
console.log(largeData[0]);
};
const btn = document.getElementById("btn");
btn.addEventListener("click", handler);
// 清理函数
return () => {
btn.removeEventListener("click", handler);
};
}
const cleanup = setupListener();
// cleanup(); // 移除监听器,largeData 可以被垃圾回收
❌ 陷阱 4:过度的闭包复杂性
// ❌ 过度复杂,难以维护
function complex() {
let a = 0;
return {
b: () => {
return {
c: () => {
return {
d: () => a++,
};
},
};
},
};
}
complex().b().c().d(); // 可读性太差
// ✅ 简化
function simple() {
let a = 0;
return {
increment: () => a++,
get: () => a,
};
}
simple().increment();
五、this 和作用域
this 的绑定规则
| 调用方式 | this 指向 | 示例 |
|---|---|---|
| 直接调用 | undefined(严格)/ window(非严格) | fn() |
| 方法调用 | 对象 | obj.method() |
| 构造函数 | 新创建的对象 | new Constructor() |
| apply/call/bind | 第一个参数 | fn.call(obj) |
| 箭头函数 | 外层的 this | () => {} |
const obj = {
name: "obj",
method() {
console.log(this); // obj
},
arrowMethod: () => {
console.log(this); // window(继承外层)
},
};
obj.method(); // obj
obj.arrowMethod(); // window
this vs 作用域的区别
// ❌ 常见混淆
const user = {
name: "Alice",
info: {
getName: function () {
console.log(this.name); // undefined(this 是 info 对象)
console.log(name); // undefined 或全局 name
},
},
};
user.info.getName();
// ✅ 理解区别
const user = {
name: "Alice",
getName: function () {
console.log(this.name); // 'Alice'(this 是 user)
return () => {
console.log(this.name); // 'Alice'(继承外层 this)
console.log(user.name); // 'Alice'(闭包引用外层变量)
};
},
};
const fn = user.getName();
fn(); // 'Alice' 'Alice'
六、10 个实战模式
1. 缓存与记忆化(Memoization)
// ✅ 缓存计算结果
function memoize(fn) {
const cache = new Map();
return (...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 expensiveAdd = memoize((a, b) => {
console.log("计算中...");
return a + b;
});
console.log(expensiveAdd(1, 2)); // "计算中..." 3
console.log(expensiveAdd(1, 2)); // 3(从缓存)
2. 函数节流(Throttle)
// ✅ 限制函数执行频率
function throttle(fn, delay) {
let lastCall = 0;
return (...args) => {
const now = Date.now();
if (now - lastCall >= delay) {
fn(...args);
lastCall = now;
}
};
}
const throttledScroll = throttle(() => {
console.log("滚动处理");
}, 1000);
window.addEventListener("scroll", throttledScroll); // 最多 1 秒触发一次
3. 防抖(Debounce)
// ✅ 延迟执行,避免频繁调用
function debounce(fn, delay) {
let timeoutId;
return (...args) => {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
fn(...args);
}, delay);
};
}
const debouncedSearch = debounce((query) => {
console.log("搜索:", query);
}, 500);
input.addEventListener("input", (e) => {
debouncedSearch(e.target.value); // 500ms 无输入后执行
});
4. 部分应用(Partial Application)
// ✅ 固定部分参数
function partial(fn, ...partialArgs) {
return (...restArgs) => {
return fn(...partialArgs, ...restArgs);
};
}
const add = (a, b, c) => a + b + c;
const add5 = partial(add, 5);
console.log(add5(10, 15)); // 30
5. 单例模式
// ✅ 确保只有一个实例
function createSingleton(Constructor) {
let instance;
return function (...args) {
if (!instance) {
instance = new Constructor(...args);
}
return instance;
};
}
class Database {
constructor(url) {
this.url = url;
console.log("连接到:", url);
}
}
const getDB = createSingleton(Database);
const db1 = getDB("localhost");
const db2 = getDB("localhost");
console.log(db1 === db2); // true
6. 装饰器模式
// ✅ 增强函数功能
function withLogging(fn) {
return (...args) => {
console.log("调用:", fn.name, args);
const result = fn(...args);
console.log("返回:", result);
return result;
};
}
const add = (a, b) => a + b;
const loggedAdd = withLogging(add);
loggedAdd(1, 2);
// 调用: add [1, 2]
// 返回: 3
7. 观察者模式
// ✅ 发布-订阅
function createEventEmitter() {
const events = new Map();
return {
on(event, handler) {
if (!events.has(event)) {
events.set(event, []);
}
events.get(event).push(handler);
},
off(event, handler) {
const handlers = events.get(event);
if (handlers) {
const index = handlers.indexOf(handler);
if (index > -1) {
handlers.splice(index, 1);
}
}
},
emit(event, ...args) {
const handlers = events.get(event) || [];
handlers.forEach((handler) => handler(...args));
},
};
}
const emitter = createEventEmitter();
emitter.on("user:login", (user) => {
console.log("欢迎:", user);
});
emitter.emit("user:login", "Alice"); // "欢迎: Alice"
8. 职责链模式
// ✅ 链式处理请求
function createHandler(condition, action, nextHandler) {
return (request) => {
if (condition(request)) {
return action(request);
}
return nextHandler ? nextHandler(request) : null;
};
}
const isVIP = (user) => user.vip;
const isAdmin = (user) => user.role === "admin";
const vipHandler = createHandler(isVIP, (user) => "优先处理", null);
const adminHandler = createHandler(isAdmin, (user) => "管理员处理", vipHandler);
const defaultHandler = createHandler(
() => true,
(user) => "普通处理",
adminHandler,
);
console.log(defaultHandler({ vip: false, role: "user" })); // "普通处理"
console.log(defaultHandler({ vip: true, role: "user" })); // "优先处理"
9. 惰性初始化(Lazy Initialization)
// ✅ 延迟加载重型资源
function lazy(fn) {
let result;
let computed = false;
return () => {
if (!computed) {
result = fn();
computed = true;
}
return result;
};
}
const getHeavyData = lazy(() => {
console.log("加载数据...");
return new Array(1000000).fill("data");
});
console.log("定义函数");
getHeavyData(); // "加载数据..."
getHeavyData(); // (无输出,直接返回)
10. 组合模式
// ✅ 组合多个小函数
function compose(...fns) {
return (x) => fns.reduceRight((acc, fn) => fn(acc), x);
}
const add1 = (x) => x + 1;
const multiply2 = (x) => x * 2;
const square = (x) => x * x;
const pipeline = compose(square, multiply2, add1);
console.log(pipeline(3)); // ((3 + 1) * 2) ^ 2 = 64
七、闭包与内存管理
何时会导致内存泄漏?
// ❌ 泄漏 1:意外的全局变量
function create() {
largeArray = new Array(1000000); // 没有 var/let,成为全局变量
}
create();
// largeArray 永远不会被垃圾回收
// ✅ 正确
function create() {
const largeArray = new Array(1000000);
}
// largeArray 随着函数执行完毕而被回收
// ❌ 泄漏 2:未清理的事件监听
const elements = document.querySelectorAll(".item");
elements.forEach((el) => {
el.addEventListener("click", () => {
console.log(el.textContent);
});
});
// 即使删除 DOM,监听器仍引用元素
// ✅ 正确:显式清理
elements.forEach((el) => {
const handler = () => console.log(el.textContent);
el.addEventListener("click", handler);
el.cleanup = () => {
el.removeEventListener("click", handler);
};
});
// 使用完后
elements.forEach((el) => el.cleanup?.());
// ❌ 泄漏 3:循环引用
const obj1 = { name: "obj1" };
const obj2 = { name: "obj2" };
obj1.ref = obj2;
obj2.ref = obj1;
// 即使没有外部引用,也无法被垃圾回收
检测内存泄漏
// 简单的内存监测
function monitorMemory(fn) {
if (performance.memory) {
const before = performance.memory.usedJSHeapSize;
fn();
const after = performance.memory.usedJSHeapSize;
console.log(`内存增长: ${(after - before) / 1024 / 1024} MB`);
}
}
monitorMemory(() => {
const arr = new Array(1000000).fill("data");
});
八、常见错误与陷阱总结
❌ 错误 1:闭包中修改外层变量
// ❌ 陷阱
for (var i = 0; i < 3; i++) {
setTimeout(() => {
i++; // 修改外层变量
console.log(i);
}, 0);
}
// 输出:1, 2, 3(而不是 0, 1, 2)
// ✅ 使用 let
for (let i = 0; i < 3; i++) {
setTimeout(() => {
console.log(i);
}, 0);
}
// 输出:0, 1, 2
❌ 错误 2:误以为每个闭包都是独立的
// ❌ 错误假设
const handlers = [];
for (let i = 0; i < 3; i++) {
handlers.push(() => i); // 所有都引用同一个 i
}
console.log(handlers[0]()); // 3(当前 i)
// ❌ 不是 0
❌ 错误 3:忽视性能成本
// ❌ 过度使用闭包
function processLarge(items) {
const cache = {};
return items.map((item) => {
return () => {
// 每个闭包都保存 cache
return cache[item.id];
};
});
}
// ✅ 只保存必要的数据
function processLarge(items) {
return items.map((item) => {
const id = item.id;
return () => id; // 只保存 id
});
}
❌ 错误 4:作用域链过长
// ❌ 作用域链太深,查找性能差
function a() {
const x = 0;
return function b() {
return function c() {
return function d() {
return function e() {
return x; // 需要向上查找 4 层
};
};
};
};
}
// ✅ 尽量扁平
function a() {
const x = 0;
return () => x; // 直接访问
}
九、最佳实践
1. 优先用 let/const,避免 var
// ✅ 现代标准
const config = {
/* ... */
};
let counter = 0;
// ❌ 过时
var oldVar = "deprecated";
2. 闭包最小化
// ❌ 暴露太多
function bad() {
const state = { a: 1, b: 2, c: 3 };
return () => state; // 暴露整个对象
}
// ✅ 只暴露必要的
function good() {
const a = 1;
return () => a; // 只暴露 a
}
3. 及时清理
// ✅ 显式清理引用
function setup() {
const data = heavyData();
const handler = () => process(data);
element.addEventListener("click", handler);
return () => {
element.removeEventListener("click", handler);
data = null; // 清理
};
}
4. 避免闭包中的 this 问题
// ✅ 用箭头函数
class MyClass {
callback = () => {
console.log(this.name);
};
}
// ✅ 或显式保存 this
class MyClass {
setup() {
const self = this;
handler.addEventListener("click", () => {
self.handle();
});
}
}
5. 选择合适的模式
// ✅ 根据需求选择
// 需要私有变量 → 模块模式
// 需要延迟计算 → 惰性初始化
// 需要组合逻辑 → 高阶函数
// 需要事件系统 → 观察者模式
十、速查表
作用域类型速查
| 作用域 | 创建方式 | 访问范围 | 何时销毁 |
|---|---|---|---|
| 全局 | 全局代码 | 所有地方 | 程序结束 |
| 函数 | 函数声明 | 函数内 | 函数返回 |
| 块级 | {} 块 | 块内 | 块结束 |
| 动态 | 函数调用 | 调用位置 | 调用结束 |
闭包检查清单
// 判断是否是闭包:
// ✅ 有内层函数
// ✅ 内层函数引用外层变量
// ✅ 外层函数已执行完毕
// ✅ 内层函数仍可访问外层变量
常见模式速查
| 模式 | 用途 | 关键字 |
|---|---|---|
| 缓存 | 避免重复计算 | Map、cache |
| 节流 | 限制执行频率 | lastCall、delay |
| 防抖 | 延迟执行 | clearTimeout |
| 模块 | 数据封装 | IIFE、return |
| 工厂 | 创建对象 | function、return |
| 单例 | 唯一实例 | instance |
| 装饰 | 增强函数 | wrapper |
| 观察 | 发布订阅 | events、Map |
十一、性能优化建议
减少闭包的性能影响
// ❌ 每次创建闭包
const arr = [1, 2, 3];
const processors = arr.map((item) => {
return () => process(item); // 每个都是新闭包
});
// ✅ 共享闭包
const process = (item) => {
/* ... */
};
const processors = arr.map((item) => process.bind(null, item));
及时释放大对象
// ✅ 使用后及时置 null
function loadData() {
const largeData = fetch("/api/data");
setTimeout(() => {
console.log(largeData);
largeData = null; // 释放
}, 100);
}
避免过深的作用域链
// ❌ 查找链太长
const result = deep.scope.chain.to.variable.x;
// ✅ 缓存中间结果
const chain = deep.scope.chain.to.variable;
const result = chain.x;
十二、总结
核心概念
- 作用域 = 变量的可访问范围
- 作用域链 = 单向向上查找的机制
- 闭包 = 函数能记住外层变量
- this = 动态指向,不是作用域
关键点
- ✅ 用
let/const创建块级作用域 - ✅ 利用闭包实现数据封装
- ✅ 避免循环中的闭包陷阱
- ✅ 及时清理事件监听器
- ✅ 注意 this 的绑定问题
常见错误 Top 5
- 循环中的闭包问题(用
let) - 忘记清理事件监听器(内存泄漏)
- this 指向混淆(用箭头函数或保存)
- 过度使用闭包(性能问题)
- 作用域链过深(查找性能差)
学习顺序
- ✅ 必学:作用域链、闭包基础
- ✅ 重要:常见陷阱、this 绑定
- ✅ 实战:10 个设计模式
- 📚 深入:性能优化、垃圾回收