闭包

45 阅读2分钟

定义

闭包是函数与其词法作用域(lexical environment)的组合,使得函数能够访问并“记住”其定义时的作用域链,即使函数在其定义的作用域外执行。

个人理解

和js本身的词法作用域有很大关系,词法作用域决定了,js作用域的关系是由编写位置决定,而不是执行位置决定 所以放闭包的内层函数引用外层函数时,在另外的外层作用域中还是可以访问到,闭包起到了对作用域内变量的一个保存作用

形成条件

  1. 内外函数嵌套;
  2. 内层函数引用外层函数的变量
  3. 返回内层函数
function outFn() {
    let innerA = 1
    return function innerFn() {
        console.log(innerA)
    }
}
let a = outFn()
a() // 1

// 错误示例 不算闭包
function outFn() {
    let innerA = 1
    function innerFn() {
        console.log(innerA)
    }
}
// 只满足了1,2两点,并未返回内层函数
// 导致了外层函数执行完之后,其作用域销毁了,内部函数和变量都被回收了

用途:

  • 读取函数内部变量: 函数外可以访问函数内变量;
  • 变量始终保持在内存中: 避免变量在函数执行后被销毁,

核心作用

  1. 可用于数据封装(私有化变量)

    • 实现模块化,隐藏内部变量,避免全局污染
    function outFn() {
        let count = 0
        return {
            getInner() {
                return count
            },
            addInner() {
                count++
            }
        }
    }
    const a = outFn()
    a.addInner()
    a.getInner()
    
  2. 保持变量状态

    • 在异步报错中保持变量状态
    for(var i = 0; i < 3; i++) {
        setTimeout(() => {console.log(i)}, 100)
    }
    
    // 使用闭包保持变量状态
    for(var i = 0; i < 3; i++) {
        (function (j) {
            setTimeout(() => {console.log(j)}, 100)
        })(i)
    }
    
  3. 函数柯里化(Currying)

    • 通过闭包分步传递参数
    function fn1(a) {
        return function fn2(b) {
            return a * b
        }
    }
    const c = fn1(2)
    c(4)
    

优点

  1. 模块化: 通过闭包隐藏内部实现,暴露接口;
  2. 灵活的状态管理: 在异步编程中保留上下文状态
  3. 避免全局污染: 通过私有变量减少命名冲突

缺点

  • 内存泄漏风险
function init() {
    const largeData = new Array(1000000).fill("data");
    document.getElementById("btn").onclick = () => {
        console.log(largeData.length); // largeData 无法释放
    };
}

最佳实践

  • 避免滥用闭包: 仅在需要封装数据和保持状态的时候使用
  • 及时释放引用: 手动解除闭包对 DOM 或 全局对象的引用
  • 模块化工具: 使用 ES6 的class 或 模块(import/export)替代闭包封装。)

闭包和词法作用域的关系

  • 词法作用域是闭包的基石:

    • 闭包的“记忆”能力完全依赖于词法作用域的静态规则。若作用域是动态的,闭包将无法正确捕获变量。
  • 闭包是词法作用域的动态表现:

    • 它通过保留作用域链,打破了词法作用域默认的生命周期限制,使得外部变量可以在函数执行后继续存活。