JavaScript闭包解析

13 阅读6分钟

闭包是 JavaScript 中既强大又危险的概念。它能让我们写出优雅的代码,也可能悄无声息地吞噬系统内存。本篇文章将深入探讨闭包的每一个角落。

前言:闭包不止是返回函数

实际开发中,很多人理解的闭包是这样的:

function outer() {
    let count = 0;
    return function() {
        count++;
        return count;
    };
}

但实际上,闭包无处不在,甚至闭包产生的结果和我们预想的结果大相庭径:

for (var i = 0; i < 5; i++) {
    setTimeout(function() {
        console.log(i); // 5,5,5,5,5
    }, 100);
}

闭包形成的必要条件

什么是闭包?

所谓闭包 ,是指那些能够访问自由变量的函数。自由变量是指在函数中使用的,但既不是函数参数,也不是函数局部变量的变量。

更通俗的理解是:闭包是函数和声明该函数的词法环境的组合。即使函数在其词法环境之外执行,它仍然可以访问该环境中的变量。

let globalFunc;

function outer() {
    const secret = "闭包的秘密";
    
    globalFunc = function() {
        console.log(secret);
    };
}

outer();
globalFunc(); // "闭包的秘密"
// 即使outer执行完毕,globalFunc仍然能访问secret

闭包形成的三个必要条件

  • 条件1:嵌套函数
  • 条件2:内部函数引用外部变量
  • 条件3:内部函数在外部作用域可访问
// 条件1:嵌套函数
function outer() {
    // 条件2:内部函数引用外部变量
    const outerVar = "外部变量";
    
    // 条件3:内部函数在外部作用域可访问
    return function inner() {
        console.log(outerVar); // 引用外部变量
    };
}

const closureFunc = outer(); // 执行outer,返回inner函数
closureFunc(); // 调用inner,仍然可以访问outerVar

不形成闭包的情况

内部函数没有引用外部变量

function noClosure() {
    let local = "局部变量";
    
    // 内部函数没有引用外部变量
    return function inner() {
        console.log("没有引用外部变量");
    }
}

内部函数引用了外部变量,但内部函数没有被返回或传递出去

function noClosure() {
    let local = "局部变量";
    
    function inner() {
        console.log(local); // 引用了外部变量
    }
    // 但内部函数没有被返回或传递出去
    inner(); // 直接在内部调用
    // 函数执行完,local被销毁
}

闭包的内存模型

闭包的内存结构

我们先通过一个复杂例子理解闭包的内存结构:

function createComplexClosure() {
    // 这些变量会被闭包捕获
    const config = { max: 100, min: 0 };
    let privateCounter = 0;
    const secretKey = "ABC-123";
    
    // 辅助函数 - 也会形成闭包
    function validate(value) {
        return value >= config.min && value <= config.max;
    }
    
    // 返回的对象方法都形成闭包
    return {
        setValue: function(value) {
            if (validate(value)) {
                privateCounter = value;
                return true;
            }
            return false;
        },
        
        increment: function() {
            if (validate(privateCounter + 1)) {
                privateCounter++;
            }
            return privateCounter;
        },
        
        getInfo: function() {
            // 注意:这个方法没有使用secretKey,但secretKey仍然被保留!
            return {
                current: privateCounter,
                config: { ...config } // 返回副本,不暴露引用
            };
        },
        
        // 这个函数使用了所有被捕获的变量
        debug: function() {
            return {
                counter: privateCounter,
                config,
                key: secretKey
            };
        }
    };
}

const obj = createComplexClosure();

内存结构分析:

  • 每个方法都有自己的函数对象
  • 但它们共享同一个词法环境(闭包)
  • 这个词法环境包含:config, privateCounter, secretKey, validate()

闭包的"隐藏"成本

在实际开发中,闭包捕获的变量可能比我们预想的要多得多:

function createHeavyClosure() {
    // 一个大对象
    const bigData = {
        items: new Array(10000).fill(null).map((_, i) => ({
            id: i,
            data: `Item ${i}`,
            meta: { created: Date.now(), tags: ['test'] }
        })),
        config: { /* ... */ }
    };
    
    // 一些原始值
    let counter = 0;
    const name = "Closure";
    
    // 返回的函数只用了counter
    return function() {
        counter++;
        console.log(`${name}: ${counter}`);
        // 注意:这里没有使用bigData!
    };
}

const lightFunc = createHeavyClosure();

上述代码中,虽然 lightFunc 只使用了 countername,但 bigData 也被闭包捕获了,无法被垃圾回收!

多个闭包共享环境

在同一个作用域中创建的多个函数,会共享闭包环境:

function createSharedClosure() {
    let sharedState = 0;
    const messages = [];
    
    function addMessage(msg) {
        messages.push(`${new Date().toISOString()}: ${msg}`);
        // 只保留最近10条
        if (messages.length > 10) {
            messages.shift();
        }
    }
    
    return {
        // 这两个方法共享同一个闭包环境
        increment: function() {
            sharedState++;
            addMessage(`Incremented to ${sharedState}`);
            return sharedState;
        },
        
        decrement: function() {
            sharedState--;
            addMessage(`Decremented to ${sharedState}`);
            return sharedState;
        },
        
        getLog: function() {
            return [...messages]; // 返回副本
        },
        
        // 这个方法会创建新的闭包
        createAction: function(actionName) {
            // 这个函数有自己的闭包(捕获actionName)
            return function() {
                sharedState = 0;
                addMessage(`Reset by ${actionName}`);
                return `Reset by ${actionName}`;
            };
        }
    };
}

const manager = createSharedClosure();
manager.increment(); // 1
manager.increment(); // 2
manager.decrement(); // 1

const resetAction = manager.createAction("Admin");
resetAction(); // sharedState被重置为0

console.log(manager.getLog());

常见闭包模式

模块模式(Module Pattern)

const Calculator = (function() {
    // 私有变量
    let memory = 0;
    const version = '1.0.0';
    
    // 私有函数
    function validateNumber(num) {
        return typeof num === 'number' && !isNaN(num);
    }
    
    // 公共接口
    return {
        add: function(a, b) {
            if (!validateNumber(a) || !validateNumber(b)) {
                throw new Error('Invalid numbers');
            }
            const result = a + b;
            memory = result; // 更新内存
            return result;
        },
        
        subtract: function(a, b) {
            if (!validateNumber(a) || !validateNumber(b)) {
                throw new Error('Invalid numbers');
            }
            const result = a - b;
            memory = result;
            return result;
        },
        
        getMemory: function() {
            return memory;
        },
        
        clearMemory: function() {
            memory = 0;
        },
        
        getVersion: function() {
            return version;
        }
    };
})();

// 使用
console.log(Calculator.add(5, 3)); // 8
console.log(Calculator.getMemory()); // 8
console.log(Calculator.getVersion()); // 1.0.0

// 无法直接访问私有成员
// Calculator.memory // undefined
// Calculator.validateNumber // undefined

柯里化(Currying)

function curry(fn) {
    return function curried(...args) {
        // 如果参数数量足够,调用原函数
        if (args.length >= fn.length) {
            return fn.apply(this, args);
        } else {
            // 参数不够,返回新函数继续接收参数
            return function(...args2) {
                // 这里形成了闭包,args被记住了
                return curried.apply(this, args.concat(args2));
            };
        }
    };
}

// 使用柯里化
function multiply(a, b, c) {
    return a * b * c;
}

const curriedMultiply = curry(multiply);

// 多种调用方式
console.log(curriedMultiply(2)(3)(4)); // 24
console.log(curriedMultiply(2, 3)(4)); // 24
console.log(curriedMultiply(2)(3, 4)); // 24

防抖(Debounce)

防抖:在事件触发后等待一段时间再执行,如果在此期间再次触发则重新计时:

function debounce(fn, delay) {
    let timer = null;
    
    return function(...args) {
        // 清除之前的定时器
        clearTimeout(timer);
        
        // 设置新的定时器
        timer = setTimeout(() => {
            fn.apply(this, args);
            timer = null;
        }, delay);
    };
}

节流(Throttle)

节流:在规定的一段时间内只执行一次:

function throttle(fn, interval) {
    let lastTime = 0;
    let timer = null;
    
    return function(...args) {
        const now = Date.now();
        const remaining = interval - (now - lastTime);
        
        if (remaining <= 0) {
            // 时间到了,立即执行
            if (timer) {
                clearTimeout(timer);
                timer = null;
            }
            fn.apply(this, args);
            lastTime = now;
        } else if (!timer) {
            // 设置定时器,在剩余时间后执行
            timer = setTimeout(() => {
                fn.apply(this, args);
                lastTime = Date.now();
                timer = null;
            }, remaining);
        }
    };
}

函数记忆(Memoization)

function memoize(fn) {
    const cache = new Map();
    
    return function(...args) {
        // 创建缓存键
        const key = JSON.stringify(args);
        
        // 如果缓存中有,直接返回
        if (cache.has(key)) {
            console.log('从缓存获取:', key);
            return cache.get(key);
        }
        
        // 否则计算并缓存
        console.log('计算并缓存:', key);
        const result = fn.apply(this, args);
        cache.set(key, result);
        return result;
    };
}

闭包的性能考量

内存泄漏的常见模式

模式1:意外的全局变量引用

function createLeakyModule() {
    const hugeData = new Array(1000000).fill("data");
    
    // 这个事件监听器形成了闭包,引用了hugeData
    document.addEventListener('click', function() {
        // 即使回调没有使用hugeData,它也被闭包捕获了!
        console.log('clicked');
    });
    
    // 解决方案:在不需要时移除事件监听器
    // 或者避免在包含大数据的函数中定义事件处理器
}

模式2:循环引用

function createCircularReference() {
    const elements = [];
    
    for (let i = 0; i < 1000; i++) {
        const element = {
            data: new Array(1000).fill('data'),
            onClick: function() {
                // 这个函数引用了elements数组
                console.log('Element clicked', elements.length);
            }
        };
        elements.push(element);
    }
    
    return elements;
    // elements数组和每个onClick函数相互引用,无法被回收
}

模式3:定时器未清理

function startPolling() {
    const data = fetchData(); // 大数据
    
    setInterval(function() {
        // 这个闭包捕获了data
        processData(data);
    }, 1000);
    
    // 如果没有clearInterval,data永远不会被释放
}

性能优化技巧

技巧1:避免不必要的闭包

function processItems(items) {
    // 不好的做法:在循环中创建闭包
    items.forEach(function(item) {
        // 这个函数创建了闭包,捕获了items
        processItem(item);
    });
    
    // 好的做法:使用箭头函数或避免创建函数
    for (let i = 0; i < items.length; i++) {
        processItem(items[i]); // 没有创建闭包
    }
}

技巧2:分离数据和逻辑

function createEfficientClosure() {
    // 大数据
    const largeData = fetchLargeData();
    
    // 提取需要的数据,而不是保留整个大数据对象
    const processedData = processData(largeData);
    
    // 立即释放对大数据的引用
    // largeData = null; // 如果不再需要
    
    return function() {
        // 只使用处理后的数据
        return operateOnData(processedData);
    };
}

技巧3:使用WeakMap/WeakSet

const weakCache = new WeakMap();

function getExpensiveValue(obj) {
    if (weakCache.has(obj)) {
        return weakCache.get(obj);
    }
    
    const value = computeExpensiveValue(obj);
    weakCache.set(obj, value);
    return value;
}

// 当obj不再被引用时,WeakMap中的条目会自动被垃圾回收

技巧4:模块模式的优化

const OptimizedModule = (function() {
    // 私有数据,但避免创建大对象
    const privateData = (function() {
        // 这里初始化私有数据
        const data = {};
        // ... 初始化逻辑
        return data;
    })();
    
    // 公共方法
    function publicMethod() {
        // 使用privateData
    }
    
    // 清理方法
    function cleanup() {
        // 清理私有数据
        for (const key in privateData) {
            delete privateData[key];
        }
    }
    
    return {
        publicMethod,
        cleanup
    };
})();

结语

闭包是JavaScript的强大特性,但需要谨慎使用,对于文章中错误的地方或者有任何问题,欢迎在评论区留言讨论!