JavaScript 闭包深入理解:作用域链、内存泄漏与经典应用

小飞兽 JavaScript 317 次阅读 2026-05-10

Introduction

闭包(Closure)是 JavaScript 最强大也最容易被误解的概念之一。当一个内部函数引用了外部函数的变量,即使外部函数已经执行完毕,这些变量依然被内部函数保留着——这就是闭包。理解闭包需要先理解作用域链(Scope Chain)和垃圾回收(GC)机制。本文从原理到实践系统讲解闭包。

闭包的形成

/* 闭包:内部函数引用了外部函数的变量 */
function outer() {
    var outerVar = '我是外层变量';
    
    function inner() {
        console.log(outerVar); // 引用了外部变量
    }
    
    return inner; // 返回内部函数
}

var fn = outer(); // outer 执行完毕
fn(); // 输出:'我是外层变量'
// outerVar 本应被 GC 回收,但因为 inner 还引用着它,所以保留下来

/* 经典计数器 */
function createCounter() {
var count = 0; // 被闭包保留,不会被 GC
return function() {
return ++count;
};
}
var counter = createCounter();
console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3

经典应用场景

/* 1. 数据私有化 */
function createUser(name) {
    var _name = name; // 私有变量
    return {
        getName: function() { return _name; },
        setName: function(n) { _name = n; }
    };
}
var user = createUser('小马');
console.log(user.getName()); // '小马'
// console.log(user._name); // undefined,无法直接访问

/* 2. 函数工厂 */
function multiply(factor) {
return function(num) { return num * factor; };
}
var double = multiply(2);
var triple = multiply(3);
console.log(double(5)); // 10
console.log(triple(5)); // 15

/* 3. 防抖(debounce)*/
function debounce(fn, delay) {
var timer = null;
return function() {
clearTimeout(timer);
timer = setTimeout(function() {
fn.apply(this, arguments);
}, delay);
};
}

循环中的闭包陷阱

/* 错误写法:所有回调都引用同一个 i */
for (var i = 1; i <= 3; i++) {
    setTimeout(function() {
        console.log(i); // 输出:4, 4, 4(因为循环结束后 i = 4)
    }, 0);
}

/* 解决方法1:用 let(块级作用域)*/
for (let i = 1; i <= 3; i++) {
setTimeout(function() {
console.log(i); // 输出:1, 2, 3(let 每次循环创建新的 i)
}, 0);
}

/* 解决方法2:IIFE 捕获当前 i 的值 */
for (var i = 1; i <= 3; i++) {
(function(captured) {
setTimeout(function() {
console.log(captured); // 1, 2, 3
}, 0);
})(i);
}

内存泄漏与闭包

/* 闭包可能导致意外的内存保留 */
function leak() {
    var bigData = new Array(1000000); // 大数组
    var leak = document.getElementById('btn');
    
    // 闭包引用了 bigData,导致整个大数组无法被 GC
    leak.addEventListener('click', function() {
        console.log(bigData.length); // 只要事件监听器在,bigData 就无法释放
    });
}

/* 解决方法:手动解除引用 */
function fixed() {
var bigData = new Array(1000000);
var leak = document.getElementById('btn');

function handler() {
console.log(bigData.length);
}

leak.addEventListener('click', handler);

// 用完后解除监听,bigData 才能被 GC
return function() {
leak.removeEventListener('click', handler);
bigData = null;
};
}

常见问题

    • 闭包与 this:内部函数的 this 默认指向 window(非严格模式),需要在外部保存 var self = this。
    • let vs var:let 是块级作用域,用 let 替代 var 可以避免大部分闭包陷阱。
    • 闭包不会导致内存泄漏:正常使用闭包不会导致问题,但闭包引用了大量 DOM 节点或大对象时要注意手动解除。
    • 块级作用域与循环:ES6 之后推荐直接用 let,在 for 循环中每次迭代都会创建新的作用域,闭包捕获的是各自的变量值。

    延伸阅读

  • You Don't Know JS:作用域与闭包