Skip to content

闭包

闭包的由来

JavaScript 中的闭包源于计算机科学中的一种理论概念,称为“λ演算”(Lambda Calculus)。λ演算是计算机科学的基础之一,1930 年由 Alonzo Church 提出,它是一种用于描述计算过程的数学抽象模型,也是函数式编程语言的基础。

在 JavaScript 中,闭包是函数和声明该函数的词法环境的组合。这个环境包含了闭包创建时所能访问的所有局部变量。

理解闭包,需要理解 JavaScript 的特性和工作原理。JavaScript 的函数在创建时,就确定了其操作的上下文环境,即词法作用域。这是因为 JavaScript 采用的是静态作用域,也叫词法作用域,函数的作用域在函数定义的时候就决定了。

javascript
function outer() {
    var name = 'JavaScript';
    function inner() {
        console.log(name);
    }
    return inner;
}
var innerFunc = outer();
innerFunc(); // 输出 'JavaScript'

outer 函数返回了 inner 函数。inner 函数访问了 outer 函数的局部变量 name,因此形成了一个闭包。即使 outer 函数执行完毕,name 变量的引用仍然被保留,因此 innerFunc 在执行时仍然能够输出 'JavaScript'。

闭包的概念虽然来自计算机科学的深层理论,但在日常的 JavaScript 编程中,它是一个非常实用且常见的特性,被广泛用于如数据隐藏和封装、模块化编程、回调函数和计时器等许多场景中。

JavaScript 中的闭包

闭包是指那些能够访问自由变量的函数。什么是自由变量呢?如果一个变量在函数内部被引用,但它既不是函数的参数也不是函数的局部变量,那么就称之为“自由变量”。

javascript
function outerFunction(outerVariable) {
    function innerFunction() {
        console.log(outerVariable);
    }
    return innerFunction;
}
var inner = outerFunction('Hello Closure');
inner(); // 输出 'Hello Closure'

outerFunction 是一个外部函数,接受一个参数 outerVariable。它包含一个内部函数 innerFunction,这个内部函数没有自己的参数或局部变量,但却引用了外部函数的变量 outerVariable。所以,我们说 innerFunction 是一个闭包,而 outerVariable 就是它的自由变量。

需要注意的是,由于 JavaScript 的垃圾回收机制,如果一个变量离开了它的作用域,那么这个变量就会被回收。但是,由于 innerFunction 是一个闭包,它引用了 outerVariable,所以即使 outerFunction 执行完毕,outerVariable 离开了它的作用域,但仍然不会被垃圾回收机制回收。

再者,每次调用外部函数,都会为内部的闭包创建一个新的作用域。

javascript
var inner1 = outerFunction('Hello Closure 1');
var inner2 = outerFunction('Hello Closure 2');
inner1(); // 输出 'Hello Closure 1'
inner2(); // 输出 'Hello Closure 2'

inner1inner2 是两个不同的闭包。他们分别有自己的作用域,储存了不同的 outerVariable

执行上下文

在 JavaScript 中,执行上下文(execution context)是一个关键概念,与闭包(closure)密切相关。

执行上下文是 JavaScript 代码执行时的环境。它包含了变量、函数声明、作用域链等信息,用于管理和跟踪代码的执行过程。当一个函数被调用时,就会创建一个新的执行上下文。每个执行上下文都有自己的词法环境(Lexical Environment),用于存储变量和函数的声明。

在理解闭包之前,让我们先了解一下执行上下文的创建和销毁过程。当函数被调用时,会创建一个新的执行上下文,并将其推入执行上下文栈(execution context stack)中。当函数执行完毕后,其执行上下文会从栈中弹出并销毁。

现在,让我们通过一个例子来更具体地了解执行上下文和闭包之间的关系:

javascript
function outerFunction(outerVariable) {
    function innerFunction(innerVariable) {
        console.log('outerVariable:', outerVariable);
        console.log('innerVariable:', innerVariable);
    }
    return innerFunction;
}
var newFunction = outerFunction('outside');
newFunction('inside'); //输出: outerVariable: outside innerVariable: inside

当调用 outerFunction 时,会创建一个新的执行上下文,其中包含了 outerVariable 参数和 innerFunction 函数声明。然后,outerFunction 返回了 innerFunction,并将其赋值给变量 newFunction

现在让我们来看看闭包是如何形成的。当 innerFunction 被返回时,它会携带其词法环境(包含 outerVariable)一起返回。这意味着 innerFunction 保持对 outerVariable的引用,即使 outerFunction 执行完毕并且其执行上下文已经销毁。

这就是闭包的力量所在。它允许内部函数(innerFunction)访问其词法环境中的变量(outerVariable),即使这些变量在其创建时的执行上下文已经不存在。

在这个例子中,newFunction 就是一个闭包。它引用了外部函数 outerFunction 的词法环境,其中包含了 outerVariable 变量。因此,当我们调用 newFunction 时,它可以访问并打印出 outerVariableinnerVariable 的值。

应用场景

数据封装和私有性

闭包可以用于创建私有变量,将变量隐藏在函数作用域内部,从而实现数据的封装和私有性。通过闭包,我们可以控制变量的访问权限,只暴露需要暴露的接口。这种封装机制可以防止外部代码直接访问和修改内部数据,增加代码的安全性。

javascript
function createCounter() {
    let count = 0;
    return {
        increment: function () {
            count++;
        },
        decrement: function () {
            count--;
        },
        getCount: function () {
            return count;
        }
    };
}
const counter = createCounter();
counter.increment();
counter.increment();
console.log(counter.getCount()); // 输出: 2

createCounter 函数返回一个对象,该对象包含了三个闭包函数,分别用于增加计数、减少计数和获取计数值。通过闭包,我们可以将 count 变量隐藏在函数内部,并通过闭包函数来操作和访问这个变量。

模块化编程

闭包可以用于实现模块化编程,将相关的变量和函数组织在一个闭包内部,形成一个模块。这样可以避免全局命名冲突,提供命名空间,并且允许模块内部的函数相互调用和共享数据。

javascript
var myModule = (function () {
    var privateVariable = '私有变量';
    function privateFunction() {
        console.log('私有函数');
    }
    return {
        publicMethod: function () {
            console.log(privateVariable);
        },
        publicFunction: function () {
            privateFunction();
        }
    };
})();
myModule.publicMethod(); // 输出: 私有变量
myModule.publicFunction(); // 输出: 私有函数

在这个例子中,我们使用了立即调用函数表达式(IIFE)来创建一个闭包,形成一个独立的模块。模块内部的变量和函数对外部是不可见的,只有通过公共接口才能访问。

回调函数和事件处理

闭包常常用于处理回调函数和事件处理,特别是在异步编程中。由于闭包的特性,它可以捕获外部函数的上下文,并在内部函数被调用时保留这个上下文,从而实现对异步操作的响应。

javascript
function fetchData(url, callback) {
    fetch(url).then(function (response) {
        return response.json();
    }).then(function (data) {
        callback(data);
    });
}
function processData(data) {
    console.log(data);
}
fetchData('https://api.example.com/data', processData);

fetchData 函数通过闭包捕获了 processData 函数作为回调函数。当异步操作完成时,它会调用回调函数并传递数据给它。闭包保持了回调函数的上下文,使得回调函数可以访问外部的 processData 函数。

缓存和记忆化

闭包还可以用于实现缓存和记忆化功能。通过闭包,我们可以在函数内部维护一个缓存,避免重复计算相同的结果,提高函数执行的性能。

javascript
function memoizedFunction() {
    var cache = {};
    return function (arg) {
        if (cache[arg]) {
            console.log('命中缓存');
            return cache[arg];
        }
        // 计算结果
        var result = arg;
        console.log('缓存结果');
        cache[arg] = result;
        return result;
    };
}
var memoized = memoizedFunction();
console.log(memoized('value')); // 第一次计算并缓存结果
console.log(memoized('value')); // 直接从缓存中读取结果

memoizedFunction 返回一个闭包函数,用于记忆化计算结果。闭包内部维护了一个缓存对象 cache,当输入相同的参数时,直接从缓存中读取结果,避免重复计算。

优缺点分析

内存消耗

闭包会导致内存占用增加,因为它们会保留对外部变量的引用,即使外部函数执行完毕。这可能会导致内存占用过高。

javascript
function createHugeArray() {
    var arr = new Array(1000000).fill('Huge Data');
    return function () {
        console.log(arr.length);
    };
}
var bigDataFunc = createHugeArray();
bigDataFunc(); // 输出: 1000000

createHugeArray 函数返回一个闭包函数,它引用了一个巨大的数组 arr。即使 createHugeArray 执行完毕,arr 仍然被闭包引用,无法被垃圾回收机制回收,从而导致内存占用增加。

内存泄漏

由于闭包会持有对外部变量的引用,如果不正确地处理闭包的使用,可能会导致内存泄漏。如果一个闭包长时间存在,但不再需要,它会一直持有对外部变量的引用,使这些变量无法被垃圾回收。

javascript
function leakMemory() {
    var data = 'Sensitive Data';
    var timer = setInterval(function () {
        console.log(data);
    }, 1000);
}
leakMemory();

leakMemory 函数创建了一个闭包,它引用了一个定时器内部的函数。即使 leakMemory 执行完毕,定时器仍然在持续执行,因此闭包会一直存在并引用 data 变量,导致 data 无法被垃圾回收。

性能影响

闭包可能对性能产生一定的影响,特别是在涉及大量变量或复杂词法环境的情况下。闭包的创建和执行可能消耗更多的时间和资源。

javascript
function calculate() {
    var result = 0;
    for (var i = 0; i < 1000000; i++) {
        result += i;
    }
    return function () {
        console.log(result);
    };
}
var expensiveFunc = calculate();
expensiveFunc(); // 输出: 499999500000

calculate 函数返回一个闭包函数,它引用了一个在循环中计算的结果。由于闭包保留了这个结果,闭包的执行可能会耗费更多的时间和资源。

解决方案

为了减少闭包的缺点,我们可以采取以下措施:

  • 优化内存使用:在闭包中避免持有大量数据或不必要的引用。确保只保留必要的变量和引用。
  • 及时清理闭包:在不需要使用闭包时,手动解除对闭包的引用,以便垃圾回收机制可以回收闭包相关的资源。
  • 避免滥用闭包:只在必要的情况下使用闭包,避免在不必要的场景中使用闭包。
  • 优化性能:在闭包的创建和使用过程中,尽量避免不必要的计算或资源消耗,以提高性能。