引言
你是否好奇:为什么有些函数执行完毕后,内部变量会自动消失,而另一些函数却能永久 "记住" 之前的数据?这背后不是魔法,而是 JavaScript 最精妙的两大设计 ------作用域链 与闭包。一个决定了变量能被谁访问,一个决定了变量能存活多久。读懂它们,你才算真正走进 JavaScript 的底层逻辑。
一、作用域链:闭包的底层根基
想要弄懂闭包,作用域链是地基,先看懂链条逻辑,闭包自然一通百通。
1.什么是作用域链:
- JS代码在预编译阶段会规划好调用栈 :每当一个函数被调用执行,就会在调用栈中新增一块「执行上下文」;每一块执行上下文由变量环境、词法环境 两部分组成。其中变量环境里自带
outer指针,固定指向外层的执行上下文。 - 当v8引擎查找变量时,会先在当前执行上下文寻找;找不到就顺着
outer指针,逐层去往外层上下文继续查找,一路追溯到全局作用域为止。
这套由outer指针首尾串联、由内向外逐层查找变量的链式结构,就叫做作用域链。
补充细节:全局执行上下文没有外层环境,它的
outer指向null。
2.还是听不懂的话也没关系,让我们来看一个例子:
js
function bar() {
console.log(myName);
}
function foo() {
var myName = '小杰'
bar()
}
var myName = '西索'
foo()
请大家思考一下这个输出的结果是什么呢?
来,让我们看看输出的结果:

3.为什么呢?让我画一张图来帮助大家理解

-
先看咱们画好的分层结构图:整张大框代表全局,内部从上到下三层分别是:bar 执行上下文(绿框)、foo 执行上下文(黄框)、全局执行上下文(蓝框) ,每个上下文都拆成「变量环境 + 词法环境」两块,黑色箭头就是关键的
outer指针。 -
outer 指针核心规则:
bar函数是在全局作用域定义 的,所以它变量环境里的outer黑色箭头,直接指向【全局执行上下文】,和bar在哪里被调用(foo 内部)毫无关系;foo同样在全局定义,它的outer也指向全局;最底层全局上下文没有外层,outer = null。 -
代码走到
bar打印myName,先在bar 自己的绿色变量环境 里找,框内没有这个变量;顺着 bar 的outer黑箭头,直接跳到最下方蓝色全局变量环境 ,找到myName = "西索";黄色 foo 框里虽然存了myName="小杰",但 bar 的outer根本不指向 foo,不在 bar 的作用域链上,访问不到 ;✅ 最终输出:
西索
关键结论:作用域链由函数定义位置决定,和函数调用位置无关。
二、闭包:作用域链催生的特殊产物
1.闭包的定义
-
常规函数销毁规则
普通函数调用执行结束后,函数内部的执行上下文连同里面定义的局部变量,会被 JS 垃圾回收机制销毁释放,变量无法再被访问。
-
闭包诞生的核心条件
依照 JS 作用域链查找规则:内层函数天生可以访问外层函数的局部变量。
如果把内层函数从外层函数内部返回到全局作用域执行 ,即便外层函数早已执行完毕、本该销毁上下文,但因为内层函数持续引用着外层变量,这些被引用的变量就不会被回收;「内层函数 + 被保留的外层变量」这个组合,就是闭包。
代码示例:
js
function foo() {
var myName = '张老师' //外层局部变量
var age = 18
function bar() { //内层函数
console.log(myName); //内层引用外层变量myName
}
return bar //把内层函数返回到全局
}
var baz = foo() //foo执行完毕,正常本该销毁内部变量
baz() //全局调用内层bar,依然能打印到:张老师
代码运行逻辑:
foo()执行完成后,本身函数体从调用栈销毁,常规情况下myName、age全部被回收;- 但
bar被赋值给全局变量baz,全局随时能调用baz; bar内部只用到了myName,因此只有被引用的 myName 被闭包留存,age 没有被引用直接正常销毁;- 后续执行
baz(),控制台正常输出:张老师。
如图所示 
外层 foo 执行完本体销毁,但因为内层 bar 还活着、还在引用myName,JS 单独开辟一块小空间 存放myName;age 没被使用,跟随 foo 原上下文正常回收。
- 这个小空间就是闭包
2. 闭包优缺点
✅ 优点:定义私有变量,防止全局变量污染
JS 原生没有私有变量关键字,利用闭包可以把数据锁在函数内部,外部代码无法直接修改内部变量,只能通过我们主动暴露的函数去读写数据,是早年 JS 实现模块化、数据私有化最主流的方案。
❌ 缺点:滥用极易造成内存泄漏
闭包会让被引用的变量长期常驻内存,无法被垃圾回收销毁;如果闭包函数长期闲置不用、开发者又没有手动切断引用,无用数据会一直占用堆内存,日积月累就引发内存泄漏。
三、经典实战:闭包解决循环取值陷阱
1. 原代码现象与错误原因
js
var arr = []
for (var i = 1; i <= 5; i++) {
arr.push(function() {
console.log(i);
})
}
// 调用全部输出:6、6、6、6、6
for (let n = 0; n < arr.length; n++) {
arr[n]()
}
输出的结果为五个6。
原因:
- 全局:因为
var i挂载在全局作用域,全程只有 1 个 i 变量 ,循环结束后i=6。 - 五个函数:循环五次往数组存入 5 个函数,所有函数的 outer 指针统一指向全局作用域,没有保存每轮循环瞬时值。
- 执行调用阶段:函数运行时顺着 outer 去全局查找 i,全局 i 早已变成 6,因此全部打印 6。
核心问题:5 个函数共用同一个全局变量
i。
2. 解决方案1:使用闭包
js
var arr = []
for (var i = 1; i <= 5; i++) {
// 每轮循环生成独立作用域
function foo(j) {
arr.push(function() {
console.log(j); //输出结果为1、2、3、4、5
})
}
foo(i) // 将当前i作为实参传入
}
for (let n = 0; n < arr.length; n++) {
arr[n]()
}
原理:每轮循环靠闭包生成独立私有变量,保存当前循环数值。
3.解决方案2:ES6 let 简化写法(底层近似闭包原理)
把var i改为let i
js
var arr = []
for(let i = 1; i <= 5; i++) {
arr.push(function(){
console.log(i);//输出:1,2,3,4,5
})
}
for(var n = 0; n <arr.length; n++) {
arr[n]()
}
let在 for 循环中,每次迭代自动生成块级独立绑定,等价于 JS 自动帮我们生成闭包保存每轮 i,无需手动写自执行函数。