JavaScript闭包指南:从作用域到闭包实战
作为 JavaScript 初学者,你是否曾被“闭包”这个概念困扰?别怕,今天我带你从作用域开始,一步步揭开闭包的神秘面纱。
一、先理解作用域:变量的“活动范围”
什么是作用域?
作用域就是变量和函数的可访问范围,它决定了变量在哪里可以被访问,在哪里会被销毁。
三种常见的作用域:
- 全局作用域:在函数外部声明的变量,随处可用。
- 函数作用域:在函数内部声明的变量,只能在该函数内部访问。
- 块级作用域:
let和const在{}内声明的变量,只在块内有效。
作用域的访问规则是:内部可以访问外部,外部不能访问内部。
二、作用域链:逐层查找的“寻宝图”
当你在当前作用域找不到变量时,JavaScript 会沿着作用域链一层层往外找,直到全局作用域为止。这种查找关系,就是作用域链。
每个函数在创建时,都会记住自己所在的作用域(词法作用域),并用一个 outer 指针指向外层作用域。
三、闭包:执行上下文的“残影”
闭包是什么?
闭包是指一个函数能够记住并访问其词法作用域中的变量,即使该函数在其词法作用域之外执行。
下面会更详细说明
闭包的形成条件:
- 函数嵌套
- 内部函数引用外部函数的变量
- 内部函数被外部函数返回,并在外部函数之外被调用
闭包的原理:
在 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 基础与实战内容。