JavaScript 闭包详解

34 阅读4分钟

什么是闭包?

闭包是指能够访问并记住其词法作用域的函数,即使这个函数在其词法作用域之外执行。

简单示例

function outer() {
    let count = 0;  // 局部变量
    
    // 内部函数形成了闭包
    function inner() {
        count++;  // 访问外部函数的变量
        console.log(count);
    }
    
    return inner;  // 返回内部函数
}

const counter = outer();  // outer 执行完毕,count 应该被销毁?
counter();  // 1 - 但 count 仍然存在!
counter();  // 2
counter();  // 3

在这个例子中,inner函数就是一个闭包,它记住了外层函数 outer的变量 count

闭包的形成条件

  1. 函数嵌套:一个函数在另一个函数内部
  2. 内部函数引用外部函数的变量
  3. 内部函数在外部函数之外被调用
function createClosure() {
    let message = "Hello";  // 闭包会"记住"这个变量
    
    return function(name) {
        console.log(`${message}, ${name}!`);  // 访问外部变量
    };
}

const greet = createClosure();
greet("Alice");  // "Hello, Alice!"
greet("Bob");    // "Hello, Bob!"

闭包的常见用途

1. 数据封装和私有变量

// 传统对象没有真正的私有属性
const person = {
    _name: "Alice",  // 下划线约定,但实际还是可访问
    getName() {
        return this._name;
    }
};
console.log(person._name);  // 可以直接访问

// 使用闭包实现真正的私有变量
function createPerson(name) {
    let privateName = name;  // 真正的私有变量
    
    return {
        getName() {
            return privateName;
        },
        setName(newName) {
            privateName = newName;
        }
    };
}

const alice = createPerson("Alice");
console.log(alice.getName());  // "Alice"
console.log(alice.privateName);  // undefined - 无法直接访问
alice.setName("Bob");
console.log(alice.getName());  // "Bob"

2. 创建函数工厂

// 创建幂函数生成器
function powerCreator(exponent) {
    return function(base) {
        return Math.pow(base, exponent);
    };
}

const square = powerCreator(2);
const cube = powerCreator(3);

console.log(square(5));  // 25
console.log(cube(3));    // 27
console.log(square(4));  // 16

3. 实现模块模式

const calculator = (function() {
    // 私有变量
    let memory = 0;
    let operationCount = 0;
    
    // 私有函数
    function incrementCount() {
        operationCount++;
    }
    
    // 返回公共接口
    return {
        add: function(a, b) {
            incrementCount();
            return a + b;
        },
        subtract: function(a, b) {
            incrementCount();
            return a - b;
        },
        getCount: function() {
            return operationCount;
        },
        clearMemory: function() {
            memory = 0;
        }
    };
})();

console.log(calculator.add(5, 3));  // 8
console.log(calculator.getCount()); // 1
console.log(calculator.memory);     // undefined - 私有变量

4. 缓存和记忆化

function createMemoizedFunction(fn) {
    const cache = {};  // 闭包缓存
    
    return function(...args) {
        const key = JSON.stringify(args);
        
        if (cache[key] !== undefined) {
            console.log('从缓存读取');
            return cache[key];
        }
        
        console.log('计算结果');
        const result = fn(...args);
        cache[key] = result;
        return result;
    };
}

// 一个计算量大的函数
function slowCalculation(n) {
    console.log(`计算 ${n} 的平方...`);
    return n * n;
}

const memoizedCalc = createMemoizedFunction(slowCalculation);
console.log(memoizedCalc(5));  // 计算 5 的平方... 25
console.log(memoizedCalc(5));  // 从缓存读取 25
console.log(memoizedCalc(6));  // 计算 6 的平方... 36

5. 事件处理和回调

function setupButtons() {
    const buttons = document.querySelectorAll('button');
    
    buttons.forEach(function(button, index) {
        // 闭包记住了每个按钮的 index
        button.addEventListener('click', function() {
            console.log(`按钮 ${index} 被点击`);
        });
    });
}

// 不使用闭包会有问题
function setupButtonsWrong() {
    const buttons = document.querySelectorAll('button');
    
    for (var i = 0; i < buttons.length; i++) {
        buttons[i].addEventListener('click', function() {
            console.log(`按钮 ${i} 被点击`);  // 总是输出最后一个 i
        });
    }
}

6. 实现柯里化

// 柯里化:将一个多参数函数转换为一系列单参数函数
function curry(fn) {
    return function curried(...args) {
        if (args.length >= fn.length) {
            return fn.apply(this, args);
        } else {
            return function(...args2) {
                return curried.apply(this, args.concat(args2));
            };
        }
    };
}

// 使用示例
function addThree(a, b, c) {
    return a + b + c;
}

const curriedAdd = curry(addThree);
const addFive = curriedAdd(2)(3);
console.log(addFive(5));  // 10
console.log(addFive(10)); // 15

闭包的原理

作用域链

function outer() {
    let x = 10;
    
    function middle() {
        let y = 20;
        
        function inner() {
            let z = 30;
            // inner 的作用域链:inner → middle → outer → global
            console.log(x + y + z);  // 可以访问所有外部变量
        }
        
        return inner;
    }
    
    return middle;
}

const innerFn = outer()();  // 得到 inner 函数
innerFn();  // 60

闭包的内存管理

function createClosures() {
    let largeData = new Array(1000000).fill('data');
    let count = 0;
    
    return {
        increment: function() {
            count++;
            console.log(count);
        },
        getData: function() {
            return largeData.length;
        }
    };
}

// 注意:即使只使用 increment,largeData 也不会被回收
// 因为闭包保持了整个作用域链的引用
const obj = createClosures();
obj.increment();  // largeData 仍在内存中!

// 正确做法:及时清理不需要的引用
function createOptimizedClosure() {
    let largeData = new Array(1000000).fill('data');
    let count = 0;
    
    const increment = function() {
        count++;
        console.log(count);
    };
    
    // 如果 largeData 不再需要,可以显式释放
    const cleanup = function() {
        largeData = null;  // 帮助垃圾回收
    };
    
    return { increment, cleanup };
}

实际应用案例

1. React Hooks 的核心

// useState 的简化实现原理
function useState(initialValue) {
    let state = initialValue;
    
    const setState = (newValue) => {
        state = newValue;
        // 触发重新渲染
        render();
    };
    
    return [state, setState];  // 闭包记住了 state
}

2. 防抖和节流

// 防抖函数
function debounce(fn, delay) {
    let timer;  // 闭包保存 timer
    
    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) {
            fn.apply(this, args);
            lastTime = now;
        }
    };
}

3. 状态管理

function createStore(reducer, initialState) {
    let state = initialState;
    let listeners = [];
    
    const getState = () => state;
    
    const dispatch = (action) => {
        state = reducer(state, action);
        listeners.forEach(listener => listener());
    };
    
    const subscribe = (listener) => {
        listeners.push(listener);
        return () => {
            listeners = listeners.filter(l => l !== listener);
        };
    };
    
    return { getState, dispatch, subscribe };
}

注意事项和最佳实践

1. 避免意外的闭包

// ❌ 意外的闭包导致内存泄漏
function createProblem() {
    const largeData = new Array(1000000);
    
    setInterval(() => {
        console.log('定时器运行');  // 闭包引用了 largeData
    }, 1000);
    
    // largeData 不会被回收,因为定时器回调形成了闭包
}

// ✅ 解决方案
function createSolution() {
    const largeData = new Array(1000000);
    
    // 立即使用并释放
    processData(largeData);
    largeData.length = 0;  // 帮助 GC
    
    setInterval(() => {
        console.log('定时器运行');
    }, 1000);
}

2. 性能优化

// ❌ 在循环中创建闭包
function slowFunction() {
    for (let i = 0; i < 10000; i++) {
        element.addEventListener('click', function() {
            console.log(i);  // 每次循环都创建新闭包
        });
    }
}

// ✅ 优化版本
function fastFunction() {
    const handler = function(e) {
        const index = e.target.dataset.index;
        console.log(index);
    };
    
    for (let i = 0; i < 10000; i++) {
        element.addEventListener('click', handler);
    }
}

闭包的作用总结

用途说明示例
数据封装创建私有变量,实现信息隐藏模块模式
状态保持函数"记住"之前的状态计数器、缓存
函数工厂动态生成特定功能的函数柯里化、防抖函数
事件处理保持事件处理器的上下文事件监听器
延迟执行异步操作保持状态Promise、setTimeout 回调
模块化组织代码,避免污染全局IIFE 模块