JavaScript闭包指南:从作用域到闭包实战

118 阅读3分钟

JavaScript闭包指南:从作用域到闭包实战

作为 JavaScript 初学者,你是否曾被“闭包”这个概念困扰?别怕,今天我带你从作用域开始,一步步揭开闭包的神秘面纱。

一、先理解作用域:变量的“活动范围”

什么是作用域?

作用域就是变量和函数的可访问范围,它决定了变量在哪里可以被访问,在哪里会被销毁。

三种常见的作用域:

  1. 全局作用域:在函数外部声明的变量,随处可用。
  2. 函数作用域:在函数内部声明的变量,只能在该函数内部访问。
  3. 块级作用域:let 和 const 在 {} 内声明的变量,只在块内有效。

作用域的访问规则是:内部可以访问外部,外部不能访问内部。

二、作用域链:逐层查找的“寻宝图”

当你在当前作用域找不到变量时,JavaScript 会沿着作用域链一层层往外找,直到全局作用域为止。这种查找关系,就是作用域链

每个函数在创建时,都会记住自己所在的作用域(词法作用域),并用一个 outer 指针指向外层作用域。

三、闭包:执行上下文的“残影”

闭包是什么?

闭包是指一个函数能够记住并访问其词法作用域中的变量,即使该函数在其词法作用域之外执行。
下面会更详细说明

闭包的形成条件:
  1. 函数嵌套
  2. 内部函数引用外部函数的变量
  3. 内部函数被外部函数返回,并在外部函数之外被调用
闭包的原理:

在 JavaScript 中,函数执行时会创建执行上下文,这个上下文包含了函数内部的变量、参数等信息。当函数执行完毕后,正常情况下它的执行上下文会从调用栈中被销毁,里面的局部变量也会被垃圾回收器回收。

但是,如果这个函数返回了一个内部函数,且内部函数引用了外部函数的变量,那么这些被引用的变量就不会被销毁,而是形成一个"闭包",继续在内存中存活,确保内部函数后续仍可访问。

代码示例:
function foo() {
    var myname = '掘金'
    var age = 18
    function bar() {
        console.log(myname);
    }
    return bar
}

var baz = foo()
baz() // 输出:掘金

foo() 执行完毕后,按理说 myname 和 age 应该被销毁,但由于 bar 引用了 myname,所以 myname 被保留在闭包中,而 age 没有被引用,会被正常回收。

四、闭包的经典应用:循环中异步引用问题

var arr = []
for (var i = 1; i <= 5; i++) {
    arr.push(function() {
        console.log(i)
    })
}

// 执行
for (let n = 0; n < arr.length; n++) {
    arr[n]() // 依次输出 6 6 6 6 6
}

为什么会这样?

  • var 声明的 i 没有块级作用域,循环结束后 i 的值是 6
  • 每个函数都引用同一个 i,执行时都输出最终值 6

解决方案:使用闭包

var arr = []
for (var i = 1; i <= 5; i++) {
    function foo(j) {
        arr.push(function() {
            console.log(j)
        })
    }
    foo(i)
}

// 执行
for (let n = 0; n < arr.length; n++) {
    arr[n]() // 依次输出 1 2 3 4 5
}

通过闭包保存每个循环中的 i 值,避免异步操作中引用错误。

五、闭包的优点与注意事项

优点:
  • 数据私有化,防止全局污染
  • 模块化开发,封装功能
  • 保存状态,实现记忆功能
注意事项:
  • 内存泄漏风险:闭包中引用的变量不会被回收,过度使用可能导致内存占用过高,合理使用,及时解除引用。

最后

闭包是 JavaScript 中强大且常见的特性,它依赖于作用域链和词法作用域。理解闭包,不仅能帮助你写出更优雅的代码,也是进阶为中级前端工程师的必经之路。
如果你觉得这篇文章对你有帮助,欢迎点赞、收藏、关注!
我正在努力学习前端知识,后续还会分享更多 JavaScript 基础与实战内容。