JavaScript 闭包详解:从原理到实践
2024/8/15大约 8 分钟
JavaScript 闭包详解:从原理到实践
什么是闭包
闭包(Closure)是 JavaScript 中一个重要且强大的概念。简单来说,闭包是指一个函数能够访问其外部作用域中变量的能力,即使在其外部函数已经执行完毕之后。
官方定义
闭包是由函数以及声明该函数的词法环境组合而成的。该环境包含了这个闭包创建时作用域内的任何局部变量。
作用域和作用域链
作用域类型
// 全局作用域
var globalVar = 'I am global';
function outerFunction() {
// 函数作用域
var outerVar = 'I am outer';
function innerFunction() {
// 函数作用域(内层)
var innerVar = 'I am inner';
// 可以访问所有外层作用域的变量
console.log(globalVar); // 'I am global'
console.log(outerVar); // 'I am outer'
console.log(innerVar); // 'I am inner'
}
return innerFunction;
}作用域链
function level1() {
var a = 1;
function level2() {
var b = 2;
function level3() {
var c = 3;
// 作用域链:level3 -> level2 -> level1 -> global
console.log(a + b + c); // 6
}
return level3;
}
return level2;
}
const func = level1()();
func(); // 6闭包的基本例子
经典示例
function createCounter() {
let count = 0;
return function() {
count++;
return count;
};
}
const counter = createCounter();
console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3
// 每个 counter 都有自己的 count 变量
const counter2 = createCounter();
console.log(counter2()); // 1
console.log(counter()); // 4为什么会形成闭包
function outerFunction(x) {
// 这个变量被内部函数引用
const outerVariable = x;
function innerFunction(y) {
// 内部函数访问外部函数的变量
console.log(outerVariable + y);
}
return innerFunction;
}
const myFunc = outerFunction(10);
myFunc(5); // 15
// 即使 outerFunction 已经执行完毕,
// innerFunction 仍然可以访问 outerVariable闭包的工作原理
执行上下文和变量对象
function createMultiplier(multiplier) {
// 当这个函数执行时,创建执行上下文
// 变量对象包含 multiplier 参数
return function(value) {
// 这个函数的作用域链包含:
// 1. 自己的变量对象 (value)
// 2. createMultiplier 的变量对象 (multiplier)
// 3. 全局变量对象
return value * multiplier;
};
}
const double = createMultiplier(2);
const triple = createMultiplier(3);
console.log(double(5)); // 10
console.log(triple(5)); // 15内存管理
function memoryExample() {
const largeData = new Array(1000000).fill('data');
return function(index) {
// 只要这个函数存在,largeData 就不会被垃圾回收
return largeData[index];
};
}
const accessor = memoryExample();
// largeData 仍然在内存中
// 如果不再需要,应该清除引用
// accessor = null; // 这样 largeData 才能被垃圾回收闭包的常见应用
1. 数据封装和私有变量
function createBankAccount(initialBalance) {
let balance = initialBalance;
return {
deposit: function(amount) {
if (amount > 0) {
balance += amount;
return balance;
}
throw new Error('Amount must be positive');
},
withdraw: function(amount) {
if (amount > 0 && amount <= balance) {
balance -= amount;
return balance;
}
throw new Error('Invalid withdrawal amount');
},
getBalance: function() {
return balance;
}
};
}
const account = createBankAccount(100);
console.log(account.getBalance()); // 100
account.deposit(50);
console.log(account.getBalance()); // 150
account.withdraw(30);
console.log(account.getBalance()); // 120
// balance 变量无法直接访问,实现了真正的私有化
// console.log(account.balance); // undefined2. 模块模式
const Calculator = (function() {
// 私有变量和方法
let result = 0;
function log(operation, value) {
console.log(`${operation}: ${value}, result: ${result}`);
}
// 公共接口
return {
add: function(value) {
result += value;
log('Add', value);
return this;
},
subtract: function(value) {
result -= value;
log('Subtract', value);
return this;
},
multiply: function(value) {
result *= value;
log('Multiply', value);
return this;
},
getResult: function() {
return result;
},
reset: function() {
result = 0;
return this;
}
};
})();
// 使用
Calculator
.add(10)
.multiply(2)
.subtract(5)
.getResult(); // 153. 工厂函数
function createValidator(rules) {
return function(value) {
const errors = [];
for (const rule of rules) {
if (!rule.test(value)) {
errors.push(rule.message);
}
}
return {
isValid: errors.length === 0,
errors: errors
};
};
}
const emailValidator = createValidator([
{
test: (value) => value && value.length > 0,
message: 'Email is required'
},
{
test: (value) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value),
message: 'Invalid email format'
}
]);
const passwordValidator = createValidator([
{
test: (value) => value && value.length >= 8,
message: 'Password must be at least 8 characters'
},
{
test: (value) => /[A-Z]/.test(value),
message: 'Password must contain uppercase letter'
},
{
test: (value) => /[0-9]/.test(value),
message: 'Password must contain number'
}
]);
console.log(emailValidator('test@example.com')); // { isValid: true, errors: [] }
console.log(passwordValidator('weak')); // { isValid: false, errors: [...] }4. 函数柯里化
function curry(fn) {
return function curried(...args) {
if (args.length >= fn.length) {
return fn.apply(this, args);
} else {
return function(...nextArgs) {
return curried.apply(this, args.concat(nextArgs));
};
}
};
}
function add(a, b, c) {
return 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
// 创建偏函数
const add5 = curriedAdd(5);
console.log(add5(2)(3)); // 105. 事件处理和回调
function createEventHandler(elementId, eventType) {
const element = document.getElementById(elementId);
let clickCount = 0;
return function(callback) {
element.addEventListener(eventType, function(event) {
clickCount++;
// 闭包保持了对 elementId, eventType, clickCount 的引用
callback({
element: elementId,
type: eventType,
count: clickCount,
event: event
});
});
};
}
const buttonHandler = createEventHandler('myButton', 'click');
buttonHandler(function(data) {
console.log(`Button ${data.element} clicked ${data.count} times`);
});6. 缓存/记忆化
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);
}
console.log('Computing...');
const result = fn.apply(this, args);
cache.set(key, result);
return result;
};
}
const expensiveFunction = memoize(function(n) {
// 模拟耗时计算
let result = 0;
for (let i = 0; i < n * 1000000; i++) {
result += i;
}
return result;
});
console.log(expensiveFunction(100)); // Computing... 然后返回结果
console.log(expensiveFunction(100)); // Cache hit! 直接返回缓存结果闭包的陷阱
1. 循环中的闭包
问题代码:
function createFunctions() {
const functions = [];
for (var i = 0; i < 3; i++) {
functions.push(function() {
console.log(i); // 所有函数都会打印 3
});
}
return functions;
}
const funcs = createFunctions();
funcs[0](); // 3
funcs[1](); // 3
funcs[2](); // 3解决方案1:使用 let
function createFunctions() {
const functions = [];
for (let i = 0; i < 3; i++) { // 使用 let 而不是 var
functions.push(function() {
console.log(i);
});
}
return functions;
}
const funcs = createFunctions();
funcs[0](); // 0
funcs[1](); // 1
funcs[2](); // 2解决方案2:使用 IIFE
function createFunctions() {
const functions = [];
for (var i = 0; i < 3; i++) {
functions.push((function(index) {
return function() {
console.log(index);
};
})(i));
}
return functions;
}解决方案3:使用 bind
function createFunctions() {
const functions = [];
for (var i = 0; i < 3; i++) {
functions.push(function(index) {
console.log(index);
}.bind(null, i));
}
return functions;
}2. 内存泄漏
// 可能导致内存泄漏的代码
function attachListeners() {
const largeData = new Array(1000000).fill('data');
document.getElementById('button').addEventListener('click', function() {
// 这个事件处理器持有对 largeData 的引用
console.log('Button clicked');
// 即使不使用 largeData,它也不会被垃圾回收
});
}
// 改进的版本
function attachListeners() {
const largeData = new Array(1000000).fill('data');
function handleClick() {
console.log('Button clicked');
}
document.getElementById('button').addEventListener('click', handleClick);
// 在适当的时候清理
return function cleanup() {
document.getElementById('button').removeEventListener('click', handleClick);
};
}
const cleanup = attachListeners();
// 稍后调用 cleanup() 来避免内存泄漏3. this 绑定问题
const obj = {
name: 'MyObject',
createFunction: function() {
return function() {
console.log(this.name); // this 不是 obj
};
},
createArrowFunction: function() {
return () => {
console.log(this.name); // this 是 obj
};
},
createBoundFunction: function() {
return function() {
console.log(this.name); // this 是 obj
}.bind(this);
}
};
const func1 = obj.createFunction();
const func2 = obj.createArrowFunction();
const func3 = obj.createBoundFunction();
func1(); // undefined(严格模式)或者 window.name
func2(); // 'MyObject'
func3(); // 'MyObject'高级应用
1. 函数式编程
// 组合函数
function compose(...functions) {
return function(value) {
return functions.reduceRight((acc, fn) => fn(acc), value);
};
}
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)² = 642. 状态机
function createStateMachine(initialState, transitions) {
let currentState = initialState;
return {
getState: () => currentState,
transition: (action) => {
const stateTransitions = transitions[currentState];
if (stateTransitions && stateTransitions[action]) {
const newState = stateTransitions[action];
console.log(`${currentState} --${action}--> ${newState}`);
currentState = newState;
return true;
}
console.log(`Invalid transition: ${action} from ${currentState}`);
return false;
},
can: (action) => {
const stateTransitions = transitions[currentState];
return stateTransitions && stateTransitions[action] !== undefined;
}
};
}
const doorMachine = createStateMachine('closed', {
closed: {
open: 'opened',
lock: 'locked'
},
opened: {
close: 'closed'
},
locked: {
unlock: 'closed'
}
});
console.log(doorMachine.getState()); // 'closed'
doorMachine.transition('open'); // closed --open--> opened
doorMachine.transition('close'); // opened --close--> closed
doorMachine.transition('lock'); // closed --lock--> locked3. 观察者模式
function createObservable() {
const observers = [];
return {
subscribe: function(callback) {
observers.push(callback);
// 返回取消订阅函数
return function unsubscribe() {
const index = observers.indexOf(callback);
if (index > -1) {
observers.splice(index, 1);
}
};
},
notify: function(data) {
observers.forEach(callback => {
try {
callback(data);
} catch (error) {
console.error('Observer error:', error);
}
});
},
getObserverCount: () => observers.length
};
}
const observable = createObservable();
const unsubscribe1 = observable.subscribe(data => {
console.log('Observer 1:', data);
});
const unsubscribe2 = observable.subscribe(data => {
console.log('Observer 2:', data);
});
observable.notify('Hello World!');
// Observer 1: Hello World!
// Observer 2: Hello World!
unsubscribe1();
observable.notify('Only Observer 2 will see this');
// Observer 2: Only Observer 2 will see this性能考虑
1. 避免不必要的闭包
// 低效:每次调用都创建新的闭包
function attachListeners(elements) {
elements.forEach(function(element, index) {
element.addEventListener('click', function() {
console.log('Clicked element', index);
});
});
}
// 高效:复用处理函数
function attachListeners(elements) {
function handleClick(event) {
const index = Array.prototype.indexOf.call(elements, event.target);
console.log('Clicked element', index);
}
elements.forEach(function(element) {
element.addEventListener('click', handleClick);
});
}2. 及时清理引用
function createTimer(callback, interval) {
let timerId = null;
let isRunning = false;
const timer = {
start: function() {
if (!isRunning) {
timerId = setInterval(callback, interval);
isRunning = true;
}
},
stop: function() {
if (isRunning) {
clearInterval(timerId);
timerId = null;
isRunning = false;
}
},
destroy: function() {
this.stop();
// 清理引用,帮助垃圾回收
callback = null;
}
};
return timer;
}最佳实践
1. 明确闭包的目的
// 好:明确的目的和清晰的接口
function createCounter(initialValue = 0, step = 1) {
let count = initialValue;
return {
increment: () => count += step,
decrement: () => count -= step,
getValue: () => count,
reset: () => count = initialValue
};
}
// 不好:模糊的目的
function createThing() {
let x = 0;
return () => ++x;
}2. 避免过度使用
// 不必要的闭包
function addNumbers(a, b) {
return function() {
return a + b;
};
}
// 简单直接
function addNumbers(a, b) {
return a + b;
}3. 文档化闭包的行为
/**
* 创建一个缓存函数,使用闭包保存计算结果
* @param {Function} fn - 要缓存的函数
* @param {Function} keyGenerator - 生成缓存键的函数
* @returns {Function} 带缓存功能的函数
*/
function memoize(fn, keyGenerator = JSON.stringify) {
const cache = new Map();
return function(...args) {
const key = keyGenerator(args);
if (cache.has(key)) {
return cache.get(key);
}
const result = fn.apply(this, args);
cache.set(key, result);
return result;
};
}总结
闭包是 JavaScript 中的核心概念,它提供了:
优势
- 数据封装:创建私有变量和方法
- 状态保持:在函数调用之间保持状态
- 模块化:创建模块和命名空间
- 函数式编程:支持高阶函数和函数组合
需要注意
- 内存使用:闭包会阻止垃圾回收
- 性能影响:过度使用可能影响性能
- 调试困难:闭包中的变量不易调试
使用建议
- 明确目的:确保闭包有明确的用途
- 及时清理:在不需要时清理引用
- 避免滥用:不要为了使用闭包而使用闭包
- 文档化:清楚地说明闭包的行为和目的
掌握闭包的概念和应用,将大大提升你的 JavaScript 编程能力,让你能够写出更加优雅和强大的代码。
