JavaScript 闭包深入理解

小飞兽 JavaScript 187 次阅读 2026-05-31

JavaScript 闭包深入理解

一、Introduction(简介)

闭包(Closure)是 JavaScript 中最强大也最容易被误解的概念之一。它是函数式编程范式的核心特性,也是许多高级设计模式(如模块模式、工厂模式、备忘录模式)的基础。

MDN 对闭包的定义是:闭包是指一个函数能够访问其词法作用域外部的变量,即使该函数在其词法作用域之外执行。 听起来很抽象,但通过本文的详细讲解和丰富的代码示例,你将对闭包有一个全面而深入的理解。

闭包不仅仅是面试中的高频考点,更是日常开发中解决变量私有化、函数工厂、数据缓存等问题的利器。理解闭包,也能帮助你更好地理解 this 的行为、垃圾回收机制,以及 JavaScript 内存管理的本质。

二、基础语法与核心概念

2.1 什么是闭包?

当一个内部函数引用了外部函数的变量,并被返回或传递到外部时,就形成了闭包。JavaScript 中的函数就是闭包——所有的函数都是闭包,因为它们都能访问创建时所在的词法作用域。

function outer() {
    const name = 'Alice';
    function inner() {
        console.log(name); // inner 引用了 outer 的变量,形成闭包
    }
    return inner;
}
const fn = outer(); // outer 已执行完毕,但 inner 仍然可以访问 name
fn(); // 输出: Alice

2.2 闭包的本质:函数 + 词法环境

每个函数在创建时,会记住创建时的作用域链(A scope chain),即所谓的词法环境。当函数在被创建时的词法作用域之外被调用时,它仍然持有对原作用域的引用,这个引用就是闭包。

function createCounter() {
    let count = 0; // 私有变量
    return {
        increment: function() { count++; return count; },
        getCount: function() { return count; }
    };
}
const counter = createCounter();
console.log(counter.increment()); // 1
console.log(counter.increment()); // 2
console.log(counter.getCount());  // 2
// count 变量被完美地私有化了,外部无法直接访问

2.3 常见创建闭包的方式

方式一:函数返回值

function multiplier(factor) {
    return function(num) {
        return num * factor;
    };
}
const double = multiplier(2);
const triple = multiplier(3);
console.log(double(5)); // 10
console.log(triple(5)); // 15

方式二:作为参数传递

function processArray(arr, callback) {
    return arr.map(callback);
}
const nums = [1, 2, 3, 4, 5];
const result = processArray(nums, function(x) {
    return x * x; // 这里形成了闭包
});
console.log(result); // [1, 4, 9, 16, 25]

方式三:定时器与事件处理

for (var i = 0; i < 3; i++) {
    setTimeout(function() {
        console.log(i); // 输出 3, 3, 3(var 没有块级作用域)
    }, 100);
}
// 解决方案:使用 let 或闭包包装
for (var i = 0; i < 3; i++) {
    (function(index) {
        setTimeout(function() {
            console.log(index); // 输出 0, 1, 2
        }, 100);
    })(i);
}

方式四:IIFE(立即执行函数表达式)

const result = (function() {
    const secret = 'hidden';
    return function() {
        return secret;
    };
})();
console.log(result()); // 'hidden'
console.log(secret);   // ReferenceError: secret is not defined

三、代码示例

3.1 模块模式(Module Pattern)

const MyModule = (function() {
    // 私有变量
    let _count = 0;
    const _cache = {};

// 私有方法
function _increment() {
_count++;
}

// 公共 API
return {
add: function(item) {
if (_cache[item]) return _cache[item];
_increment();
_cache[item] = { id: _count, data: item };
return _cache[item];
},
getCount: function() {
return _count;
},
getCache: function() {
return { ..._cache };
}
};
})();

console.log(MyModule.add('apple')); // { id: 1, data: 'apple' }
console.log(MyModule.add('banana')); // { id: 2, data: 'banana' }
console.log(MyModule.add('apple')); // { id: 1, data: 'apple' } — 缓存命中
console.log(MyModule.getCount()); // 2
console.log(_count); // ReferenceError — 私有变量无法访问

3.2 函数工厂(Function Factory)

function makeAdder(x) {
    return function(y) {
        return x + y;
    };
}
const add5 = makeAdder(5);
const add10 = makeAdder(10);

console.log(add5(3)); // 8
console.log(add10(3)); // 13
console.log(add5(10)); // 15

// 每个闭包都有自己独立的 x 值,互不干扰

3.3 内存泄漏与闭包陷阱

function leakExample() {
    const largeArray = new Array(1000000).fill('x');
    const element = document.getElementById('myElement');

element.addEventListener('click', function() {
// 闭包引用了 largeArray,即使事件处理器从未被调用,
// largeArray 也无法被垃圾回收,造成内存泄漏
console.log(largeArray.length);
});
// 修复:使用完后手动清理,或在事件处理器中避免引用大对象
return function cleanup() {
element.removeEventListener('click', /* 引用同一函数 */);
};
}

3.4 用闭包实现私有方法

class Stack {
    constructor() {
        let _items = []; // 私有属性,闭包保护

this.push = function(item) {
_items.push(item);
return this;
};
this.pop = function() {
return _items.pop();
};
this.peek = function() {
return _items[_items.length - 1];
};
this.getSize = function() {
return _items.length;
};
}
}

const stack = new Stack();
stack.push(1).push(2).push(3);
console.log(stack.pop()); // 3
console.log(stack.peek()); // 2
console.log(stack._items); // undefined — 无法直接访问

3.5 once 函数的实现(保证函数只执行一次)

function once(fn) {
    let called = false;
    let result;
    return function(...args) {
        if (!called) {
            called = true;
            result = fn.apply(this, args);
        }
        return result;
    };
}

const init = once(function(config) {
console.log('初始化执行:', config);
return 'initialized';
});

console.log(init('config1')); // 初始化执行: config1 -> 'initialized'
console.log(init('config2')); // 无输出 -> 'initialized'(不重复执行)
console.log(init('config3')); // 无输出 -> 'initialized'

四、运行效果

在浏览器环境中运行上述代码,可以观察到以下行为:

    • 模块模式:私有变量 _count_cache 无法从外部访问,实现了真正的数据封装
    • 函数工厂add5add10 各自持有独立的 x 值,输出不同的结果
    • 内存泄漏示例:在 DevTools Memory 面板中可以看到堆内存持续增长(在大对象场景下)
    • 私有方法stack._items 返回 undefined,证明闭包实现了真正的私有化

    通过 Chrome DevTools 的 Sources 面板,可以在闭包创建时查看其词法环境的 Scope 面板,直观看到闭包引用的变量。

    五、常见问题与注意事项

    Q1:闭包会造成内存泄漏吗?

    闭包本身不会导致内存泄漏,但不当使用会。例如在循环中创建闭包引用大对象,或在 DOM 事件处理中忘记移除监听器,都会阻止垃圾回收。现代浏览器(Chrome 66+)对闭包优化较好,但仍然需要注意及时清理引用。

    Q2:var 循环中的闭包问题

    在 ES6 之前,常见的问题是循环中的闭包引用同一个 var 变量,导致所有回调共享同一个值。ES6 的 let 提供了块级作用域,彻底解决了这一问题。

    // var 版本(错误)
    for (var i = 0; i < 3; i++) {
        setTimeout(() => console.log(i), 100); // 3, 3, 3
    }
    

    // let 版本(正确)
    for (let i = 0; i < 3; i++) {
    setTimeout(() => console.log(i), 100); // 0, 1, 2
    }

    Q3:闭包与 this 的关系

    闭包中的 this 指向通常不是内部函数本身的 this,而是外层函数的 this(词法 this)。需要使用 bind / call / apply 或箭头函数来正确绑定 this

    const obj = {
        name: 'obj',
        regular: function() {
            const self = this;
            return function() {
                console.log(self.name); // 'obj',self 捕获了外层 this
            };
        },
        arrow: function() {
            return () => {
                console.log(this.name); // 'obj',箭头函数自动绑定外层 this
            };
        }
    };
    

    Q4:闭包与垃圾回收

    当闭包引用了外部函数的变量时,这些变量不会被垃圾回收,直到闭包本身被销毁。开发中应当注意:不再需要的闭包应当及时解除引用(例如移除事件监听器),以便垃圾回收器回收内存。

    Q5:闭包性能问题

    每个闭包都会在内存中保留其词法环境链,如果创建大量闭包(例如在循环中创建数组),会消耗较多内存。应避免在性能敏感的循环中创建不必要的闭包。

    六、延伸阅读

    • MDN Web DocsClosures — 官方权威文档,详细讲解闭包的用法和细节
    • Kyle Simpson(You Don't Know JS 系列)You Don't Know JS Yet — Scope & Closures 卷,深入剖析闭包原理
    • JavaScript 秘密花园JavaScript Garden — 中文资料,讲解闭包和 this 的常见陷阱
  • Philip Roberts完整理解闭包(演讲) — 视觉化展示闭包和执行上下文的关系
  • 现代 JavaScript 教程闭包 — 现代 JavaScript 教程 — 中文友好,从基础到高级应用

闭包是 JavaScript 的灵魂之一。理解它不仅能帮助我们写出更优雅的代码,也能让我们更深入地理解语言的本质。建议在日常开发中多尝试使用闭包解决实际问题,熟能生巧。