“我是闭包,您哪位”

在 JavaScript 语言中,闭包一直被任务是一个既强大又易混淆的概念

什么是闭包?

闭包简单来说就是 "函数与其引用的词法环境的组合"。当一个函数被定义时,他的作用域链就被确定下来,即使这个函数在定义时的作用域已经销毁,闭包依然能够让函数访问这些被捕获的变量。

简单定义

闭包就是一个函数以及创建该函数时所处的词法作用域,确保函数能够持续访问这些变量。

备注:这种特性使得函数不仅仅是一个代码块,而是携带了其执行上下文的完整信息。

词法作用域与执行上下文

理解闭包需要先掌握的两个重要概念

词法作用域
  • 定义:词法作用域是指变量的作用域在代码编写时就已经确定,而不是在运行时动态决定的。也就是说,函数内部能访问哪些外部变量由函数定义时的位置决定。

  • 示例

    js 复制代码
    function outer(){
        let a = 10;
        function inner() {
            console.log(a) // inner 函数可以访问 outer 中的变量a
        }
        inner()
    }
    outer()

备注:由于词法作用域的存在,函数在被定义时就已经携带了它所能访问的变量信息,这为闭包的形成奠定了基础

执行上下文
  • 定义:执行上下文是 JavaScript 中代码执行时所处的环境。它包含了变量对象、作用域链、this指向等信息。
  • 作用:当一个函数被调用时,会创建一个新的执行上下文,并将其压入执行栈中。闭包正是利用了这些执行上下文中的变量。

备注:当函数返回后,其执行上下文通常会被销毁,但如果返回的函数仍然引用了这个上下文中的变量,那么这些变量就不会被垃圾回收,形成闭包。

闭包的实现原理

闭包的核心在于: 函数内部定义的子函数可以访问外部函数中的局部变量,即使外部函数已经执行完毕。

实现过程
  • 定义一个函数,并在其中声明局部变量。
  • 在该函数内部定义另一个函数,该内部函数可以访问外部函数的变量。
  • 将内部函数返回到外部,使其在外部执行时仍然能够访问原有的变量。
示例代码
js 复制代码
function createCount() {
    let count = 0; // 外部函数的局部变量
    return function() { // 返回的内部函数构成闭包
        count++
        console.log(count)
    }
}

const counter = createCount()
counter() // 1
counter() // 2

备注:上面的例子中,内部函数一直可以访问createCount中的变量count,即使 createCount 已经执行完毕。这就是闭包的实际表现。

闭包的应用场景

数据封装和私有变量

闭包可以用来模拟私有变量,实现数据封装。

js 复制代码
function Person(name) {
    let _name = name; // 私有变量
    
    return {
        getName: function() {
            return _name;
        },
        setName: function(newName) {
            _name = newName;
        }
    }
}

const person = Person('Alice');
console.log(person.getName()); // Alice
person.setName('Bob');
console.log(person.getName()); // Bob

备注:通过闭包,可以避免直接访问对象内部的私有数据,只能 通过特定的方法进行操作。

创建函数工厂

利用闭包可以创建灵活的工厂函数,生成带有特定状态的函数实例。

js 复制代码
function makeAdder(x) {
    return function(y) {
        return x + y;
    }
}

const add5 = makeAdder(5);
console.log(add5(10)); // 15

备注:工厂函数利用闭包保存了参数 x 的值,使得返回的函数能够"记住"这个值

常见问题和注意事项

内存泄漏

由于闭包会持有外部函数的变量引用,若使用不当,可能导致内存无法及时释放。

  • 解决办法
    • 避免在不必要时创建过多闭包。
    • 在合适的时机手动清除闭包中不再需要的变量引用。
循环中的闭包问题

在循环中使用闭包时,由于变量捕获可能导致意外的结果。传统使用 var 定义变量时,所有闭包共享同一个变量。

js 复制代码
for (var i = 0; i < 3; i++){
    setTimeout(function() {
        console.log(i); // 循环结束后,i 的值为3,因此每次输出 3
    }, 100)
}

解决方法:

  • 使用 let 替换 var,因为let块级作用域使每次循环都有独立的变量。

    js 复制代码
    for (let i = 0; i < 3; i++){
        setTimeout(function() {
            console.log(i); // 0, 1, 2
        }, 100)
    }
  • 使用 IIFE (立即调用函数表达式)来捕获变量

    js 复制代码
    for (var i = 0; i < 3; i++) {
        (function(j){
            setTimeout(function() {
                console.log(j); // 0, 1, 2
            }, 100)
        })(i)
    }

备注:在使用闭包时,务必注意作用域问题,合理选择变量声明方式,避免意外捕获相同的变量。

其他相关知识点

高阶函数
  • 定义:高阶函数是指能够接受函数作为参数或返回函数的函数。闭包常用于实现高阶函数,使得函数可以保存和操作状态。

  • 示例

    js 复制代码
    function multiplier(factor) {
        return function(number) {
            return number + factor;
        }
    }
    
    const double = multiplier(2);
    console.log(double(2)); // 10
IIFE(立即调用函数表达式)
  • 用途:IIFE 可以创建一个独立的作用域,常用于避免变量污染全局作用域,并借助闭包保存局部变量。

  • 示例

    js 复制代码
    (function() {
        let message == 'Hello, World!';
        console.log(message);
    })();
块级作用域与 let/const

区别: var声明的变量具有函数作用域,而 letconst则具有块级作用域,这在使用闭包时尤为重要,能避免因变量共享而产生的问题。

备注:掌握不同变量声明方式的作用域规则,是正确使用闭包的前提

相关推荐
轻语呢喃9 分钟前
DeepSeek 接口调用:从 HTTP 请求到智能交互
javascript·deepseek
风之舞_yjf40 分钟前
Vue基础(14)_列表过滤、列表排序
前端·javascript·vue.js
belldeep1 小时前
QuickJS 如何发送一封邮件 ?
javascript·curl·smtp·quickjs
BillKu1 小时前
scss(sass)中 & 的使用说明
前端·sass·scss
疯狂的沙粒1 小时前
uni-app 项目支持 vue 3.0 详解及版本升级方案?
前端·vue.js·uni-app
Jiaberrr2 小时前
uniapp Vue2 获取电量的独家方法:绕过官方插件限制
前端·javascript·uni-app·plus·电量
谢尔登2 小时前
【React】React 18 并发特性
前端·react.js·前端框架
Joker`s smile2 小时前
使用React+ant Table 实现 表格无限循环滚动播放
前端·javascript·react.js
国家不保护废物2 小时前
🌟 React 魔法学院入学指南:从零构建你的第一个魔法阵(项目)!
前端·react.js·架构
然我2 小时前
从原生 JS 到 React:手把手带你开启 React 业务开发之旅
javascript·react.js·前端框架