玩转 JavaScript 闭包

引言

在编程里面,闭包是一个很强大的概念。它允许开发者创建能够访问并操作其创建环境中变量的函数。闭包帮助我们写出更加高效,灵活的代码。还能让开发者更好地掌握变量和函数的作用域。

什么是闭包?

  • 定义闭包:

    闭包是一个函数与该函数可以访问的词法环境的组合。

    闭包其实简而言之来说就是内部的函数可以调用外部函数的变量(即使外部函数执行完毕,它所适用的局部变量会被销毁。)

  • 词法环境和作用域链的概念:

    对于大多数现代编程语言来说,每个函数都有自己的作用域,其中包含它可以直接访问的变量集合。词法环境就是函数创建的时候作用域组成起来的。当一个函数作为另外一个函数的内部函数的时候,内部函数会继承外部函数的作用域,形成了一个作用域链。这个链使得内部函数可以访问外部函数的局部变量。即使外层函数已经执行完毕,这些内部函数仍然保留着对外部变量的引用。

  • 示例:

    function closure(params) {
      return function innerClosure(param) {
        console.log("closure:", params);
        console.log("innerClosure:", param);
      }
    }
    
    // 闭包的调用
    const fn = closure("hello");
    fn("world");
    

    在这个例子里面,首先我们传入外层函数的变量为 hello,接着 外层函数执行结束,并将 innerClosure 赋值给 fn 再执行 fn 函数,传入变量 world 给内部变量,最后我们发现打印在这里插入图片描述这就代表了 fn 就是一个闭包,包含了 closureinnerClosure 两个函数的词法环境,因此能够记住外层的 paramhello

闭包的工作原理

  • 变量的作用域和生命周期:

    对于作用域来说,变量的作用域控制了变量可被访问和操作的范围。在不同的作用域中声明的变量只能在那个作用域内被访问。

    对于生命周期来说,一般变量的销毁随着函数的运行结束而销毁。当然在我们的闭包中,外部函数即使销毁,内部函数会得到外部函数的变量的引用。

  • 内部函数如何访问外部函数的局部变量:

    闭包之所以能访问外部函数的局部变量,是因为它保留了对该变量的引用,并将其作为自身词法环境的一部分,所以这也就是为什么内部函数可以访问和修改外部变量。

  • 示例:

    function createCounter() {
      let count = 0; // 外部函数的局部变量
    
      return function () { // 内部函数
        count += 1;
        console.log(count);
      };
    }
    
    const counter = createCounter(); // 创建闭包
    counter(); // 输出 1
    counter(); // 输出 2
    counter(); // 输出 3
    

    在这个例子里面, createCounter 函数返回一个内部函数,这个内部函数可以访问并修改 createCounter 中的局部变量 count。每次调用 counter(),它都会增加 count 的值并输出,即使 createCounter 函数早已执行完毕。

闭包的使用场景

  • 回调函数

    在异步编程中使用闭包保持状态。

    比如说现在有一个 table 循环展示的时候,它的操作里面有按钮,在原生的 JavaScript 之中,如果没有使用闭包,其中所有的事件处理器都引用同一个变量,导致所有处理器在执行时都打印最后一个按钮的索引,因为该变量在循环结束后保留了最后的值。

    在使用闭包的时候,每个按钮都有一个唯一的索引,当按钮被点击时,闭包确保了正确的索引被记录下来,即使事件处理器在所有按钮上都是相同的函数实例。

  • 私有变量和方法

    通过闭包实现模块模式,保护变量不被外部直接访问。

    // 私有变量和方法
    const myMoudle = (function () {
      let privateVariable = 0;
    
      function privateMethod() {
        console.log('这是一个私有方法');
        console.log(privateVariable);
      }
    
      return {
        publicMethod: privateMethod
      }
    })();
    myMoudle.publicMethod(); // 输出 '这是一个私有方法'
    console.log(myModule.privateVariable);  // 报错,无法访问私有变量
    

    通过闭包实现私有化的方法,这里我们设计了一个 (function () {...})(); —— 这个函数表达式后面跟着一对圆括号,表示这个函数立即执行。为什么要这么做呢?可以立即准备好供外部代码使用,无需等待或显式调用。

  • 迭代器和生成器

    使用闭包保存状态信息。

    关于迭代器和生成器后面我会专门写一篇博客来玩转这两个概念。

  • 装饰器和高阶函数

    利用闭包增强或修改函数的行为。

    // 装饰器和高阶函数
    function addDecorator(decorator) {
      return function (...args) {
        console.log(`函数:${decorator.name}, 参数:`, args);
        return decorator(...args);
      }
    }
    
    const add = function (a, b) {
      return a + b;
    };
    const addWithLogging = addDecorator(add);
    addWithLogging(1, 2); // 输出 '函数:add, 参数: [1, 2]'
    

    闭包作为实现模块模式的基础,使得代码变得更加健壮,更加模块化。

实战案例分析

  • 避免全局变量污染:

    其实就是上面代码中的私有变量和方法,定义的局部变量不会成为全局变量,只通过对外开放的接口来对定义在函数内部的局部变量进行修改,这样就不会污染全局作用域了。

  • 实现数据封装:

    闭包也可以用来实现数据封装,保护变量不被外部代码直接访问或修改,这有助于代码的安全性和稳定性。下面是一个简单的例子:

    function BankAccount(initialBalance) {
      let balance = initialBalance;
    
      this.deposit = function (amount) {
        balance += amount;
      };
    
      this.withdraw = function (amount) {
        if (amount <= balance) {
          balance -= amount;
        }
      };
    
      this.getBalance = function () {
        return balance;
      };
    }
    
    const account = new BankAccount(100);
    account.deposit(50);
    account.withdraw(20);
    console.log(account.getBalance()); // 输出 130
    

闭包的陷阱和常见问题

  • 内存泄漏问题:

    其实这个问题我们自然而然也能够想到,因为我们闭包的内部函数会使用到外部变量,持有外部函数的变量的引用,即使外部函数已经执行完毕。如果闭包中的函数很少被调用,或者引用的变量非常大(如大型数组或 DOM 节点),那么这些变量可能会占用不必要的内存空间。

  • 闭包与垃圾回收机制的关系:

    垃圾回收机制负责自动释放不再使用的内存。然而,当一个闭包中的函数引用外部变量时,只要这个函数还存在,外部变量就不会被垃圾回收器回收。因此,要避免内存泄漏,就需要确保闭包中的函数最终会被销毁,或者在不再需要外部变量时,手动解除引用。

    一般手动解除引用也很简单直接设置为 null 即可。

最佳实践

  1. 明确意图:在你使用闭包的时候,应该用于封装状态和实现私有变量,而不是仅仅为了使用而使用。

  2. 限制作用域:尽量限制闭包的范围。不要让闭包持有不必要的全局或长生命周期的引用,尤其是大对象或DOM元素,这可能会导致内存泄漏。

  3. 避免循环中创建闭包:在循环中创建闭包时要小心,因为这可能导致所有闭包共享相同的外部变量引用。例如,在 JavaScript 中,使用 IIFE(立即执行函数表达式)来创建独立的闭包,以避免“闭包陷阱”。

    举例:

    for (var i = 0; i < 10; i++) {
        setTimeout(function() {
            console.log(i); // 这里期望输出0到9,但实际上输出10十次
        }, 100 * i);
    }
    

    优化过后:

    for (var i = 0; i < 10; i++) {
        (function(i) {
            setTimeout(function() {
                console.log(i); // 正确地输出0到9
            }, 100 * i);
        })(i);
    }
    

    每个setTimeout回调函数都有其自己的i值,这个值在 IIFE 执行时被立即捕获,从而避免了闭包陷阱。

结论

  • 上面我介绍了闭包的基本概念,闭包是什么,闭包的工作流程

  • 其实闭包在我们日常的开发之中是非常强大的,包括封装变量和函数,以及实现私有变量和方法,保护内部的状态不被外部代码直接访问。

  • 闭包可以用于保存状态信息,确保在回调函数或后续调用中可以访问到这些状态。

  • 闭包是实现高阶函数和装饰器模式的关键,可以用来增强或修改函数的行为,而无需修改原函数的代码。

  • 当然我们在使用闭包的时候也需要注意一些细节:

    1. 避免闭包陷阱:在循环中创建闭包时要特别小心,确保每个闭包都有其独立的作用域,避免所有闭包共享相同变量引用的问题。
    2. 性能考量:闭包的使用可能会影响性能,特别是在大规模应用中,过多的闭包可能导致内存消耗增大,应适度使用。
    3. 内存管理:理解闭包如何影响垃圾回收机制,避免不必要的变量引用,及时解除不再需要的引用,防止内存泄漏。

所以我们要学会闭包,掌握闭包,运用闭包。

希望对您的学习有帮助,有什么问题欢迎大家一起交流!

  • 26
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值